diff --git a/Source/UShaderLab/Private/ShaderLabImportResolver.cpp b/Source/UShaderLab/Private/ShaderLabImportResolver.cpp index 8e4526f..0249004 100644 --- a/Source/UShaderLab/Private/ShaderLabImportResolver.cpp +++ b/Source/UShaderLab/Private/ShaderLabImportResolver.cpp @@ -36,11 +36,6 @@ namespace ShaderLabResolver_Private return P.ToLower(); } - static bool IsTexture(const FShaderLabProperty& P) - { - return P.Type == EShaderLabPropertyType::Texture2D || P.Type == EShaderLabPropertyType::TextureCube; - } - struct FResolveState { const FShaderLabImportResolver::FSourceLoader& Loader; @@ -267,7 +262,12 @@ bool FShaderLabImportResolver::Resolve( continue; } Lib.PropRewrite.Add(Prop.Name, Promoted); - if (IsTexture(Prop)) + // A texture property of ANY kind (2D/Cube/2DArray/3D/CubeArray) carries a companion + // `Sampler`; its promoted name must be rewritten in lockstep with the texture, or a + // namespaced import leaves the body's `XSampler` unrewritten while the emitted parameter is + // `NS_XSampler` -> undefined symbol. Use the canonical predicate (ShaderLabModel.h) so this + // stays in step with the full set of texture types. + if (ShaderLabIsTextureType(Prop.Type)) { Lib.PropRewrite.Add( FName(*(Prop.Name.ToString() + TEXT("Sampler"))), diff --git a/Source/UShaderLab/Private/ShaderLabModule.cpp b/Source/UShaderLab/Private/ShaderLabModule.cpp index d26e414..9392bd5 100644 --- a/Source/UShaderLab/Private/ShaderLabModule.cpp +++ b/Source/UShaderLab/Private/ShaderLabModule.cpp @@ -16,9 +16,9 @@ DEFINE_LOG_CATEGORY_STATIC(LogShaderLab, Log, All); void FShaderLabModule::StartupModule() { #if !WITH_EDITOR - // In cooked builds there is no editor graph builder; apply material settings when the registry - // builds a base shell. The shell carries no shader map — instances carry their own. - FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddLambda( + // In cooked builds there is no graph builder; apply material settings when the registry builds a base + // shell. The shell carries no shader map — instances carry their own. + BuildHandle = FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddLambda( [](UMaterial& Material, const FShaderLabModel& Model) { FShaderLabRuntimeBuilder::ApplySettings(Material, Model); @@ -29,6 +29,13 @@ void FShaderLabModule::StartupModule() void FShaderLabModule::ShutdownModule() { +#if !WITH_EDITOR + if (BuildHandle.IsValid()) + { + FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle); + BuildHandle.Reset(); + } +#endif } IMPLEMENT_MODULE(FShaderLabModule, UShaderLab) diff --git a/Source/UShaderLab/Private/ShaderLabSubsystem.cpp b/Source/UShaderLab/Private/ShaderLabSubsystem.cpp index 2b133e2..9407952 100644 --- a/Source/UShaderLab/Private/ShaderLabSubsystem.cpp +++ b/Source/UShaderLab/Private/ShaderLabSubsystem.cpp @@ -42,10 +42,13 @@ void UShaderLabSubsystem::DiscoverAndRegisterAll() bScanned = true; // Both editor and cooked runtime rebuild from the SAME .usl sources — there is no serialized - // side data. The .usl files are staged into packaged builds (a wildcard RuntimeDependency in - // ShaderLab.Build.cs) and the parser/discovery are runtime-safe, so a cooked build re-parses - // them exactly as the editor does. In the editor the bound builder compiles a graph; in a cooked - // build it only applies settings to a never-rendered base shell (the instances carry the shaders). + // side data. The .usl files are staged into packaged builds by UShaderLab.Build.cs, which ENUMERATES + // each file into RuntimeDependencies and registers the source dirs as ExternalDependencies so new/removed + // files invalidate the makefile (NOT a wildcard RuntimeDependency — that was the classic trap that + // silently dropped newly added .usl; see Build.cs + AIDoc §5). The parser/discovery are runtime-safe, so + // a cooked build re-parses them exactly as the editor does. In the editor (and `-game`) the bound builder + // compiles a graph; in a cooked build it only applies settings to a never-rendered base shell (the + // instances carry the shaders). const TArray Files = FShaderLabDiscovery::FindShaderLabFiles(); int32 NumOk = 0; for (const FString& File : Files) diff --git a/Source/UShaderLab/Public/ShaderLabMaterialRegistry.h b/Source/UShaderLab/Public/ShaderLabMaterialRegistry.h index d557e42..abbcef7 100644 --- a/Source/UShaderLab/Public/ShaderLabMaterialRegistry.h +++ b/Source/UShaderLab/Public/ShaderLabMaterialRegistry.h @@ -15,6 +15,14 @@ class UMaterial; */ DECLARE_MULTICAST_DELEGATE_TwoParams(FOnShaderLabBuildMaterial, UMaterial& /*Material*/, const FShaderLabModel& /*Model*/); +/** + * Broadcast AFTER a base material's graph has been fully (re)built and compiled by the uncooked builder. + * Split from OnBuildMaterial so the editor-only reaction (refreshing open Material Instance editors) runs + * strictly after the build, without dragging the MaterialEditor dependency into the UncookedOnly builder + * module (which must load in `-game`, where editor-UI modules are absent). No listeners in `-game`. + */ +DECLARE_MULTICAST_DELEGATE_OneParam(FOnShaderLabMaterialGraphBuilt, UMaterial& /*Material*/); + /** * Owns the in-memory base materials. They live in a compiled-in package named "/Script/UShaderLab" * so that saved assets (MICs) can hard-reference them by object path and have that import resolve @@ -55,11 +63,15 @@ public: FOnShaderLabBuildMaterial& OnBuildMaterial() { return BuildMaterialDelegate; } + /** Fired by the uncooked builder once a base material's graph is built + compiled (see delegate doc). */ + FOnShaderLabMaterialGraphBuilt& OnMaterialGraphBuilt() { return MaterialGraphBuiltDelegate; } + /** Models registered so far, keyed by object name (used by the instance factory picker and the example/validation commandlets). */ const TMap& GetRegisteredModels() const { return RegisteredModels; } private: UPackage* Package = nullptr; FOnShaderLabBuildMaterial BuildMaterialDelegate; + FOnShaderLabMaterialGraphBuilt MaterialGraphBuiltDelegate; TMap RegisteredModels; }; diff --git a/Source/UShaderLab/Public/ShaderLabModule.h b/Source/UShaderLab/Public/ShaderLabModule.h index a157bdf..41145ec 100644 --- a/Source/UShaderLab/Public/ShaderLabModule.h +++ b/Source/UShaderLab/Public/ShaderLabModule.h @@ -10,4 +10,11 @@ class FShaderLabModule : public IModuleInterface public: virtual void StartupModule() override; virtual void ShutdownModule() override; + +#if !WITH_EDITOR +private: + // Handle for the cooked-runtime OnBuildMaterial->ApplySettings binding, unbound in ShutdownModule + // (symmetric with the builder/editor modules). Only exists in cooked builds where that binding is made. + FDelegateHandle BuildHandle; +#endif }; diff --git a/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.cpp b/Source/UShaderLabBuilder/Private/MaterialExpressionShaderLabParameterAnchor.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.cpp rename to Source/UShaderLabBuilder/Private/MaterialExpressionShaderLabParameterAnchor.cpp diff --git a/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.h b/Source/UShaderLabBuilder/Private/MaterialExpressionShaderLabParameterAnchor.h similarity index 100% rename from Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.h rename to Source/UShaderLabBuilder/Private/MaterialExpressionShaderLabParameterAnchor.h diff --git a/Source/UShaderLabEditor/Private/ShaderLabAnchorCapabilityRegistry.cpp b/Source/UShaderLabBuilder/Private/ShaderLabAnchorCapabilityRegistry.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabAnchorCapabilityRegistry.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabAnchorCapabilityRegistry.cpp diff --git a/Source/UShaderLabBuilder/Private/ShaderLabBuilderModule.cpp b/Source/UShaderLabBuilder/Private/ShaderLabBuilderModule.cpp new file mode 100644 index 0000000..9275d15 --- /dev/null +++ b/Source/UShaderLabBuilder/Private/ShaderLabBuilderModule.cpp @@ -0,0 +1,167 @@ +// Copyright UShaderLab. All Rights Reserved. + +#include "Engine/Engine.h" +#include "HAL/FileManager.h" +#include "Interfaces/IPluginManager.h" +#include "MaterialShared.h" +#include "Materials/Material.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "ShaderCore.h" +#include "ShaderLabGraphBuilder.h" +#include "ShaderLabMaterialInstanceConstant.h" +#include "ShaderLabMaterialRegistry.h" +#include "ShaderLabModel.h" +#include "UObject/UObjectIterator.h" + +DEFINE_LOG_CATEGORY_STATIC(LogShaderLabBuilder, Log, All); + +namespace +{ + /** + * Fill a base material shell: rebuild its expression graph from the model, then recompile it and every + * dependent instance. Runs whenever the registry asks (startup discovery + editor hot reload) in ANY + * uncooked run — the editor AND a `-game` launch on the editor binary. In `-game` the project is uncooked, + * so each instance must translate its own static-permutation shader map from THIS graph at load: an empty + * shell here is exactly why materials used to fall back to the default material in `-game`. + */ + void BuildAndCompile(UMaterial& Material, const FShaderLabModel& Model) + { + // Absolute, forward-slashed source path for the poison node's `#line` mapping (matches the graph + // builder's own convention so compiler errors click through to the .usl). + FString SrcPath = FPaths::ConvertRelativePathToFull(Model.SourceFilePath); + SrcPath.ReplaceInline(TEXT("\\"), TEXT("/")); + + if (Model.LoadErrors.Num() > 0) + { + // Parse-stage failure: the .usl never produced a valid model. Route the diagnostics through + // the shader compiler as a poison material so they surface in the MIC editor / cook, not just + // the log. + for (const FString& E : Model.LoadErrors) + { + UE_LOG(LogShaderLabBuilder, Error, TEXT("ShaderLab parse '%s': %s"), *Model.ShaderName, *E); + } + FShaderLabGraphBuilder::BuildPoisonInto(Material, Model.LoadErrors, SrcPath); + } + else + { + TArray Errors; + if (!FShaderLabGraphBuilder::BuildInto(Material, Model, Errors)) + { + for (const FString& E : Errors) + { + UE_LOG(LogShaderLabBuilder, Error, TEXT("ShaderLab build '%s': %s"), *Model.ShaderName, *E); + } + // BuildInto already cleared the graph before failing — don't leave the base silently empty. + // Replace it with a poison material so the build errors compile-fail with the same + // visibility as an HLSL error, instead of vanishing into the log. + FShaderLabGraphBuilder::BuildPoisonInto(Material, Errors, SrcPath); + } + } + + // Recompile the base AND propagate to everything that depends on it. A bare PostEditChange + // recompiles only the base shell; the instances carry their own static-permutation shader maps + // (and the level components render those), so on a hot-reload they must be recompiled and + // re-pushed too. FMaterialUpdateContext does exactly that on scope exit: recompile the listed + // material, then recache every dependent instance and refresh the components/viewports using it. + { + FMaterialUpdateContext UpdateContext; + UpdateContext.AddMaterial(&Material); + Material.PreEditChange(nullptr); + Material.PostEditChange(); + } + + // Re-derive the cook-inclusion data (CachedExpressionData copy + profile overrides) on every ROOT + // UShaderLabMaterialInstanceConstant of this base. The FMaterialUpdateContext above recompiles the + // dependent shader maps but does NOT refresh that derived data (it's built once at PostLoad and pinned by + // bLoadedCachedExpressionData=true), so a hot-reload that added/removed an MPC or profile would otherwise + // leave a resident MIC stale — and an in-editor iterative cook would serialize the stale references. + // + // Inheritance chain: only a ROOT (immediate parent == this base) carries the derived data; CHILD instances + // (parent is another instance, however deep) hold none and resolve up the chain to the root at query time, + // and their shared shader maps were just recompiled by FMaterialUpdateContext — so refreshing the roots is + // sufficient and children need no separate pass. RefreshShaderLabDerivedData self-guards via + // GetShaderLabRootBase(), so it is a safe no-op on anything that isn't a root of this base. Skip CDOs and + // objects being GC'd. + for (TObjectIterator It( + /*AdditionalExclusionFlags*/ RF_ClassDefaultObject, + /*bIncludeDerivedClasses*/ true, + /*InternalExclusionFlags*/ EInternalObjectFlags::Garbage); It; ++It) + { + if (It->Parent == &Material) + { + It->RefreshShaderLabDerivedData(); + } + } + + // The graph is now built + compiled and the derived data refreshed. Notify the editor layer so it can + // refresh any open Material Instance editors — kept out of this UncookedOnly module because that reaction + // needs the MaterialEditor module, which is absent in `-game`. No listeners there; a no-op broadcast. + FShaderLabMaterialRegistry::Get().OnMaterialGraphBuilt().Broadcast(Material); + } +} + +class FShaderLabBuilderModule : public IModuleInterface +{ +public: + virtual void StartupModule() override + { + // Map the plugin's Shaders/ directory so generated Custom nodes can #include + // "/Plugin/ShaderLab/Private/ShaderLabCommon.ush" at shader-compile time. + if (const TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab"))) + { + const FString ShaderDir = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders")); + if (!AllShaderSourceDirectoryMappings().Contains(TEXT("/Plugin/ShaderLab"))) + { + AddShaderSourceDirectoryMapping(TEXT("/Plugin/ShaderLab"), ShaderDir); + } + } + + // Map the project's Shaders/ directory to the virtual root "/Project" so user .ush library + // files placed there (alongside .usl) can be #included from a shader's Includes { } block. + { + const FString ProjectShaderDir = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders")); + if (FPaths::DirectoryExists(ProjectShaderDir) && !AllShaderSourceDirectoryMappings().Contains(TEXT("/Project"))) + { + AddShaderSourceDirectoryMapping(TEXT("/Project"), ProjectShaderDir); + } + } + + // Map the plugin's Intermediate/ShaderLabGen directory (a build-time artifact dir) so the graph + // builder can #include per-shader generated local-code headers (`/UShaderLabGen/.gen.ush`). + // AddShaderSourceDirectoryMapping requires the real dir to already exist, so create it first. + { + const FString GenDir = FShaderLabGraphBuilder::GetGeneratedShaderDir(); + const TCHAR* GenRoot = FShaderLabGraphBuilder::GetGeneratedVirtualRoot(); + if (!GenDir.IsEmpty()) + { + IFileManager::Get().MakeDirectory(*GenDir, /*Tree*/ true); + if (FPaths::DirectoryExists(GenDir) && !AllShaderSourceDirectoryMappings().Contains(GenRoot)) + { + AddShaderSourceDirectoryMapping(GenRoot, GenDir); + } + } + } + + // Fill in / recompile base materials whenever the registry asks (startup + hot reload). This is the + // binding that was previously in the Editor module and never fired in `-game`; hosting it here (a + // module loaded for all uncooked runs) is the fix for invisible materials under `-game`. + BuildHandle = FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddStatic(&BuildAndCompile); + + UE_LOG(LogShaderLabBuilder, Log, TEXT("ShaderLabBuilder module started.")); + } + + virtual void ShutdownModule() override + { + if (BuildHandle.IsValid()) + { + FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle); + BuildHandle.Reset(); + } + } + +private: + FDelegateHandle BuildHandle; +}; + +IMPLEMENT_MODULE(FShaderLabBuilderModule, UShaderLabBuilder) diff --git a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp b/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp similarity index 99% rename from Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp index 1f125f0..a7b84c0 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp +++ b/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp @@ -41,7 +41,6 @@ #include "ShaderLabIntrinsicRegistry.h" #include "ShaderLabAnchorCapabilityRegistry.h" #include "ShaderLabImportResolver.h" -#include "ShaderLabRuntimeBuilder.h" #include "UObject/Class.h" #include "ShaderLabSettingsApplier.h" #include "UObject/UObjectGlobals.h" @@ -130,31 +129,9 @@ namespace ShaderLabGraph return Full; } - /** - * Resolve a virtual shader path ("/Project/Lib/X.uslfunc", "/Plugin/ShaderLab/...", "/Engine/...") to an - * absolute disk path via the editor's registered shader-source directory mappings (longest-prefix match). - * Returns false if no mapping root contains the path. Used to load `.uslfunc` imports off disk. - */ - static bool ResolveVirtualShaderFile(const FString& VirtualPath, FString& OutDiskPath) - { - FString Best, BestDir; - for (const TPair& Pair : AllShaderSourceDirectoryMappings()) - { - if ((VirtualPath.StartsWith(Pair.Key + TEXT("/")) || VirtualPath == Pair.Key) && Pair.Key.Len() > Best.Len()) - { - Best = Pair.Key; - BestDir = Pair.Value; - } - } - if (Best.IsEmpty()) - { - return false; - } - FString Rest = VirtualPath.Mid(Best.Len()); - Rest.RemoveFromStart(TEXT("/")); - OutDiskPath = FPaths::Combine(BestDir, Rest); - return true; - } + // Virtual-shader-path -> disk resolution is now the shared FShaderLabGraphBuilder::ResolveVirtualShaderFile + // (defined below, exposed in the header) so the editor module (hot-reload loader) and the VSCode button + // reuse the same longest-prefix-match logic instead of each hand-rolling it. /** * Wrap a user HLSL body so shader-compiler errors map back to the .usl source: a `#line` @@ -2356,6 +2333,27 @@ namespace ShaderLabGraph } } +bool FShaderLabGraphBuilder::ResolveVirtualShaderFile(const FString& VirtualPath, FString& OutDiskPath) +{ + FString Best, BestDir; + for (const TPair& Pair : AllShaderSourceDirectoryMappings()) + { + if ((VirtualPath.StartsWith(Pair.Key + TEXT("/")) || VirtualPath == Pair.Key) && Pair.Key.Len() > Best.Len()) + { + Best = Pair.Key; + BestDir = Pair.Value; + } + } + if (Best.IsEmpty()) + { + return false; + } + FString Rest = VirtualPath.Mid(Best.Len()); + Rest.RemoveFromStart(TEXT("/")); + OutDiskPath = FPaths::Combine(BestDir, Rest); + return true; +} + bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabModel& Model, TArray& OutErrors) { using namespace ShaderLabGraph; diff --git a/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIDEPrepare.cpp similarity index 97% rename from Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIDEPrepare.cpp index 13fe19a..5b7ec0d 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp +++ b/Source/UShaderLabBuilder/Private/ShaderLabIDEPrepare.cpp @@ -6,7 +6,9 @@ // 1. /Shaders/Private/ShaderLabUENode.ush — the UE_ node-intrinsic stubs (+ their enums) // generated from FShaderLabIntrinsicRegistry. (The static // ShaderLab.ush umbrella includes it; it is IDE-only.) -// 2. /.vscode/settings.json — merges (non-destructively) files.associations +// 2. /Shaders/Private/UEFunctions/Transform.ush + TransformPosition.ush — the generated +// UE_Transform{From}VectorTo{To} / ...PositionTo{To} stubs (all space pairs). +// 3. /.vscode/settings.json — merges (non-destructively) files.associations // (*.usl/*.uslfunc/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping // (virtual shader roots -> disk, from AllShaderSourceDirectoryMappings). // pathRemapping is machine-specific; the command regenerates it. @@ -414,7 +416,7 @@ namespace ShaderLabIDEPrepare_Private static void Run(const TArray& /*Args*/) { const TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab")); - check(Plugin.IsValid()); // The command lives in this plugin's editor module; it must be findable. + check(Plugin.IsValid()); // The command lives in this plugin's builder module; it must be findable. const FString PluginShaderDir = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders")); const FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()); @@ -430,5 +432,5 @@ namespace ShaderLabIDEPrepare_Private static FAutoConsoleCommand GShaderLabIDEPrepareCommand( TEXT("ShaderLab.IDE.Prepare"), - TEXT("Generate IDE completion assets for .usl: /Shaders/ShaderLab.ush, shadertoolsconfig.json, .vscode/settings.json."), + TEXT("Generate IDE completion assets for .usl: Private/ShaderLabUENode.ush, Private/UEFunctions/Transform.ush + TransformPosition.ush, and .vscode/settings.json."), FConsoleCommandWithArgsDelegate::CreateStatic(&ShaderLabIDEPrepare_Private::Run)); diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsicRegistry.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsicRegistry.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics.h similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics.h diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Atmosphere.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Atmosphere.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Decal.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Decal.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_DistanceField.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_DistanceField.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_DistanceField.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_DistanceField.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Instancing.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Instancing.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Mesh.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Mesh.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Object.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Object.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Particle.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Particle.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Time.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_Time.cpp diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_View.cpp similarity index 100% rename from Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp rename to Source/UShaderLabBuilder/Private/ShaderLabIntrinsics_View.cpp diff --git a/Source/UShaderLabEditor/Public/ShaderLabAnchorCapabilityRegistry.h b/Source/UShaderLabBuilder/Public/ShaderLabAnchorCapabilityRegistry.h similarity index 98% rename from Source/UShaderLabEditor/Public/ShaderLabAnchorCapabilityRegistry.h rename to Source/UShaderLabBuilder/Public/ShaderLabAnchorCapabilityRegistry.h index 9fcc852..3a51531 100644 --- a/Source/UShaderLabEditor/Public/ShaderLabAnchorCapabilityRegistry.h +++ b/Source/UShaderLabBuilder/Public/ShaderLabAnchorCapabilityRegistry.h @@ -79,7 +79,7 @@ struct FShaderLabAnchorCapability * Distance Field) on first use; other editor modules may register their own from their StartupModule via * Get().Register(...). The graph builder queries it once per build. */ -class USHADERLABEDITOR_API FShaderLabAnchorCapabilityRegistry +class USHADERLABBUILDER_API FShaderLabAnchorCapabilityRegistry { public: static FShaderLabAnchorCapabilityRegistry& Get(); diff --git a/Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h b/Source/UShaderLabBuilder/Public/ShaderLabGraphBuilder.h similarity index 65% rename from Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h rename to Source/UShaderLabBuilder/Public/ShaderLabGraphBuilder.h index 75a07bb..af39d94 100644 --- a/Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h +++ b/Source/UShaderLabBuilder/Public/ShaderLabGraphBuilder.h @@ -8,15 +8,16 @@ class UMaterial; struct FShaderLabModel; /** - * Editor-only builder that turns a parsed FShaderLabModel into a UMaterial expression graph: - * property parameter nodes + a Custom HLSL node for the Surface body wired into a Substrate - * Slab BSDF (-> FrontMaterial), plus an optional Vertex Custom node for WPO/displacement/UVs. + * Turns a parsed FShaderLabModel into a UMaterial expression graph: property parameter nodes + a Custom + * HLSL node for the Surface body wired into a Substrate Slab BSDF (-> FrontMaterial), plus an optional + * Vertex Custom node for WPO/displacement/UVs. Lives in the UncookedOnly UShaderLabBuilder module, so it + * runs in the editor AND under `-game` on uncooked data (not in cooked builds — see AIDoc §2.9). * * The build is fully deterministic for a given model (node creation order, input/output order, * generated HLSL) so the cook-time and editor-time graphs — and therefore the baked uniform * expression set — match. */ -class USHADERLABEDITOR_API FShaderLabGraphBuilder +class USHADERLABBUILDER_API FShaderLabGraphBuilder { public: /** @@ -36,6 +37,15 @@ public: */ static void BuildPoisonInto(UMaterial& Material, const TArray& Diagnostics, const FString& SrcPath); + /** + * Resolve a virtual shader path ("/Project/Lib/X.uslfunc", "/Plugin/ShaderLab/...", "/Engine/...") to an + * absolute disk path via the registered shader-source directory mappings (longest-prefix match, exact-root + * allowed). Returns false if no mapping root contains the path. The single canonical reversal of the + * shader-source mappings — used to load `.uslfunc` imports, by the editor hot-reload loader, and by the + * VSCode button. (Forward direction: FShaderLabMaterialRegistry::MakeVirtualShaderPath.) + */ + static bool ResolveVirtualShaderFile(const FString& VirtualPath, FString& OutDiskPath); + /** * Virtual shader root under which per-shader generated local-code headers live * (e.g. "/UShaderLabGen"). The editor module maps it to GetGeneratedShaderDir() at startup. diff --git a/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h b/Source/UShaderLabBuilder/Public/ShaderLabIntrinsicRegistry.h similarity index 98% rename from Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h rename to Source/UShaderLabBuilder/Public/ShaderLabIntrinsicRegistry.h index 2daa7f2..4f00097 100644 --- a/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h +++ b/Source/UShaderLabBuilder/Public/ShaderLabIntrinsicRegistry.h @@ -86,7 +86,7 @@ struct FShaderLabIntrinsicDesc * other editor modules can register intrinsics for their own custom UMaterialExpression nodes from * their StartupModule via Get().Register(...). */ -class USHADERLABEDITOR_API FShaderLabIntrinsicRegistry +class USHADERLABBUILDER_API FShaderLabIntrinsicRegistry { public: static FShaderLabIntrinsicRegistry& Get(); diff --git a/Source/UShaderLabBuilder/UShaderLabBuilder.Build.cs b/Source/UShaderLabBuilder/UShaderLabBuilder.Build.cs new file mode 100644 index 0000000..2c5f508 --- /dev/null +++ b/Source/UShaderLabBuilder/UShaderLabBuilder.Build.cs @@ -0,0 +1,40 @@ +// Copyright UShaderLab. All Rights Reserved. + +using UnrealBuildTool; + +// The material-graph-building core: turns a parsed .usl model into a UMaterial expression graph and +// triggers its compile. This is needed whenever the engine runs UNCOOKED data — the editor, AND a +// `-game` / Standalone launch on the editor binary against an uncooked project (GIsEditor == false). +// It must NOT be an `Editor`-type module (those are gated on GIsEditor and never load in `-game`, which +// left base materials as empty shells and made instances fall back to the default material). `UncookedOnly` +// loads iff `!RequiresCookedData()`, i.e. exactly the uncooked runs, and is excluded from cooked builds +// (where instances carry their own shader maps and the runtime module's ApplySettings path suffices). +// +// Deliberately depends only on Engine/RenderCore/RHI/Projects/Json — NO UnrealEd/MaterialEditor/Slate — +// so it is safe to load in `-game` where those editor-UI modules are not initialized. The editor-only UX +// (factory, VSCode button, hot-reload watcher, open-editor refresh) stays in UShaderLabEditor. +public class UShaderLabBuilder : ModuleRules +{ + public UShaderLabBuilder(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + bUseUnity = false; + + PublicDependencyModuleNames.AddRange(new string[] + { + "Core", + "CoreUObject", + "Engine", + }); + + PrivateDependencyModuleNames.AddRange(new string[] + { + "UShaderLab", + "RenderCore", + "RHI", + "Json", + "Projects", + }); + } +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp b/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp index 0171c60..199462c 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp @@ -3,108 +3,42 @@ #include "DirectoryWatcherModule.h" #include "Editor.h" #include "Engine/Engine.h" -#include "HAL/FileManager.h" #include "IDirectoryWatcher.h" -#include "Interfaces/IPluginManager.h" #include "MaterialEditingLibrary.h" -#include "MaterialShared.h" #include "Materials/Material.h" +#include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" -#include "ShaderCore.h" -#include "Misc/FileHelper.h" #include "ShaderLabDiscovery.h" #include "ShaderLabGraphBuilder.h" #include "ShaderLabImportResolver.h" -#include "ShaderLabMaterialInstanceConstant.h" #include "ShaderLabMaterialRegistry.h" #include "ShaderLabModel.h" #include "ShaderLabParser.h" #include "ShaderLabSubsystem.h" #include "ShaderLabVSCodeButton.h" -#include "UObject/Package.h" -#include "UObject/UObjectIterator.h" DEFINE_LOG_CATEGORY_STATIC(LogShaderLabEditor, Log, All); +// This module hosts only the interactive-editor UX for ShaderLab (GIsEditor-gated): the .usl directory +// watcher for hot reload, the "open in VSCode" toolbar button, the Material Instance factory, and +// refreshing open Material Instance editors after a rebuild. The material-graph-building core moved to +// UShaderLabBuilder (an UncookedOnly module) so it also runs under `-game`, where this Editor module and +// its editor-UI dependencies (UnrealEd/MaterialEditor) are absent. + namespace { - /** Editor-side reaction to a base material needing build: rebuild graph + trigger compile. */ - void BuildAndCompile(UMaterial& Material, const FShaderLabModel& Model) + /** + * Editor-only reaction to a base material finishing its (re)build: refresh any open Material Instance + * editors so newly added/removed parameters (scalars, textures, static-bool switches) appear without + * reopening the editor. RegenerateArrays pulls the parameter set from the freshly rebuilt parent; mirrors + * what the stock material editor does after RecompileMaterial. Bound to OnMaterialGraphBuilt (not + * OnBuildMaterial) so it runs strictly AFTER the builder finished the graph + compile. Guarded on GEditor: + * the builder also runs during cook/headless registration and under `-game`, where there is no interactive + * editor (and RebuildMaterialInstanceEditors dereferences GEditor). + */ + void RefreshInstanceEditors(UMaterial& Material) { - // Absolute, forward-slashed source path for the poison node's `#line` mapping (matches the graph - // builder's own convention so compiler errors click through to the .usl). - FString SrcPath = FPaths::ConvertRelativePathToFull(Model.SourceFilePath); - SrcPath.ReplaceInline(TEXT("\\"), TEXT("/")); - - if (Model.LoadErrors.Num() > 0) - { - // Parse-stage failure: the .usl never produced a valid model. Route the diagnostics through - // the shader compiler as a poison material so they surface in the MIC editor / cook, not just - // the log. - for (const FString& E : Model.LoadErrors) - { - UE_LOG(LogShaderLabEditor, Error, TEXT("ShaderLab parse '%s': %s"), *Model.ShaderName, *E); - } - FShaderLabGraphBuilder::BuildPoisonInto(Material, Model.LoadErrors, SrcPath); - } - else - { - TArray Errors; - if (!FShaderLabGraphBuilder::BuildInto(Material, Model, Errors)) - { - for (const FString& E : Errors) - { - UE_LOG(LogShaderLabEditor, Error, TEXT("ShaderLab build '%s': %s"), *Model.ShaderName, *E); - } - // BuildInto already cleared the graph before failing — don't leave the base silently empty. - // Replace it with a poison material so the build errors compile-fail with the same - // visibility as an HLSL error, instead of vanishing into the log. - FShaderLabGraphBuilder::BuildPoisonInto(Material, Errors, SrcPath); - } - } - - // Recompile the base AND propagate to everything that depends on it. A bare PostEditChange - // recompiles only the base shell; the instances carry their own static-permutation shader maps - // (and the level components render those), so on a hot-reload they must be recompiled and - // re-pushed too. FMaterialUpdateContext does exactly that on scope exit: recompile the listed - // material, then recache every dependent instance and refresh the components/viewports using it. - { - FMaterialUpdateContext UpdateContext; - UpdateContext.AddMaterial(&Material); - Material.PreEditChange(nullptr); - Material.PostEditChange(); - } - - // Re-derive the cook-inclusion data (CachedExpressionData copy + profile overrides) on every ROOT - // UShaderLabMaterialInstanceConstant of this base. The FMaterialUpdateContext above recompiles the - // dependent shader maps but does NOT refresh that derived data (it's built once at PostLoad and pinned by - // bLoadedCachedExpressionData=true), so a hot-reload that added/removed an MPC or profile would otherwise - // leave a resident MIC stale — and an in-editor iterative cook would serialize the stale references. - // - // Inheritance chain: only a ROOT (immediate parent == this base) carries the derived data; CHILD instances - // (parent is another instance, however deep) hold none and resolve up the chain to the root at query time, - // and their shared shader maps were just recompiled by FMaterialUpdateContext — so refreshing the roots is - // sufficient and children need no separate pass. RefreshShaderLabDerivedData self-guards via - // GetShaderLabRootBase(), so it is a safe no-op on anything that isn't a root of this base. Skip CDOs and - // objects being GC'd. - for (TObjectIterator It( - /*AdditionalExclusionFlags*/ RF_ClassDefaultObject, - /*bIncludeDerivedClasses*/ true, - /*InternalExclusionFlags*/ EInternalObjectFlags::Garbage); It; ++It) - { - if (It->Parent == &Material) - { - It->RefreshShaderLabDerivedData(); - } - } - - // Refresh any open Material Instance editors built on this base so newly added/removed parameters - // (scalars, textures, static-bool switches) appear without reopening the editor. RegenerateArrays - // pulls the parameter set from the freshly rebuilt parent; mirrors what the stock material editor - // does after RecompileMaterial. Must run after PostEditChange so the parent's parameters are current. - // Guarded on GEditor: this handler also runs during cook/headless registration, where there is no - // interactive editor (and RebuildMaterialInstanceEditors dereferences GEditor). if (GEditor) { UMaterialEditingLibrary::RebuildMaterialInstanceEditors(&Material); @@ -117,45 +51,8 @@ class FShaderLabEditorModule : public IModuleInterface public: virtual void StartupModule() override { - // Map the plugin's Shaders/ directory so generated Custom nodes can #include - // "/Plugin/ShaderLab/Private/ShaderLabCommon.ush" at shader-compile time. - if (const TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab"))) - { - const FString ShaderDir = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders")); - if (!AllShaderSourceDirectoryMappings().Contains(TEXT("/Plugin/ShaderLab"))) - { - AddShaderSourceDirectoryMapping(TEXT("/Plugin/ShaderLab"), ShaderDir); - } - } - - // Map the project's Shaders/ directory to the virtual root "/Project" so user .ush library - // files placed there (alongside .usl) can be #included from a shader's Includes { } block. - { - const FString ProjectShaderDir = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders")); - if (FPaths::DirectoryExists(ProjectShaderDir) && !AllShaderSourceDirectoryMappings().Contains(TEXT("/Project"))) - { - AddShaderSourceDirectoryMapping(TEXT("/Project"), ProjectShaderDir); - } - } - - // Map the plugin's Intermediate/ShaderLabGen directory (a build-time artifact dir) so the graph - // builder can #include per-shader generated local-code headers (`/UShaderLabGen/.gen.ush`). - // AddShaderSourceDirectoryMapping requires the real dir to already exist, so create it first. - { - const FString GenDir = FShaderLabGraphBuilder::GetGeneratedShaderDir(); - const TCHAR* GenRoot = FShaderLabGraphBuilder::GetGeneratedVirtualRoot(); - if (!GenDir.IsEmpty()) - { - IFileManager::Get().MakeDirectory(*GenDir, /*Tree*/ true); - if (FPaths::DirectoryExists(GenDir) && !AllShaderSourceDirectoryMappings().Contains(GenRoot)) - { - AddShaderSourceDirectoryMapping(GenRoot, GenDir); - } - } - } - - // Fill in / recompile base materials whenever the registry asks (startup + hot reload). - BuildHandle = FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddStatic(&BuildAndCompile); + // After the builder rebuilds a base material's graph, refresh any open MIC editors on it. + GraphBuiltHandle = FShaderLabMaterialRegistry::Get().OnMaterialGraphBuilt().AddStatic(&RefreshInstanceEditors); StartWatchingSources(); @@ -169,10 +66,10 @@ public: { StopWatchingSources(); ShaderLabVSCodeButton::Unregister(); - if (BuildHandle.IsValid()) + if (GraphBuiltHandle.IsValid()) { - FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle); - BuildHandle.Reset(); + FShaderLabMaterialRegistry::Get().OnMaterialGraphBuilt().Remove(GraphBuiltHandle); + GraphBuiltHandle.Reset(); } } @@ -219,25 +116,6 @@ private: WatchedRoots.Reset(); } - /** Resolve a virtual shader path to disk via the registered shader-source directory mappings. */ - static bool ResolveVirtualToDisk(const FString& VirtualPath, FString& OutDiskPath) - { - FString Best, BestDir; - for (const TPair& Pair : AllShaderSourceDirectoryMappings()) - { - if ((VirtualPath.StartsWith(Pair.Key + TEXT("/")) || VirtualPath == Pair.Key) && Pair.Key.Len() > Best.Len()) - { - Best = Pair.Key; - BestDir = Pair.Value; - } - } - if (Best.IsEmpty()) { return false; } - FString Rest = VirtualPath.Mid(Best.Len()); - Rest.RemoveFromStart(TEXT("/")); - OutDiskPath = FPaths::Combine(BestDir, Rest); - return true; - } - static FString NormalizePath(const FString& Path) { FString N = FPaths::ConvertRelativePathToFull(Path); @@ -269,7 +147,7 @@ private: FShaderLabImportResolver::FSourceLoader Loader = [](const FString& VPath, FString& OutSrc, FString& OutDisk, FString& OutErr) -> bool { - if (!ResolveVirtualToDisk(VPath, OutDisk)) { OutErr = TEXT("unmapped"); return false; } + if (!FShaderLabGraphBuilder::ResolveVirtualShaderFile(VPath, OutDisk)) { OutErr = TEXT("unmapped"); return false; } if (!FFileHelper::LoadFileToString(OutSrc, *OutDisk)) { OutErr = TEXT("read failed"); return false; } return true; }; @@ -331,7 +209,7 @@ private: } } - FDelegateHandle BuildHandle; + FDelegateHandle GraphBuiltHandle; TMap WatchedRoots; }; diff --git a/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp b/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp index 8b9eab3..551959e 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp @@ -8,7 +8,7 @@ #include "MaterialEditorModule.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" -#include "ShaderCore.h" +#include "ShaderLabGraphBuilder.h" #include "ShaderLabMaterialInstanceConstant.h" #include "Styling/AppStyle.h" #include "Textures/SlateIcon.h" @@ -22,21 +22,16 @@ namespace ShaderLabVSCodeButton { static FDelegateHandle GExtenderDelegateHandle; - /** Reverse the ShaderLabPath virtual path (e.g. "/Project/Examples/Basic.usl") to a disk path via the - * registered shader mappings (generic — /Project, /Plugin/, /Engine). Empty if unresolved. */ + /** Reverse the ShaderLabPath virtual path (e.g. "/Project/Examples/Basic.usl") to an absolute disk path + * via the shared shader-mapping resolver (generic — /Project, /Plugin/, /Engine). Empty if unresolved. */ static FString ResolveVirtualToDisk(const FString& VirtualPath) { - int32 BestKeyLen = -1; - FString BestDisk; - for (const TPair& Mapping : AllShaderSourceDirectoryMappings()) + FString Disk; + if (!FShaderLabGraphBuilder::ResolveVirtualShaderFile(VirtualPath, Disk)) { - if (VirtualPath.StartsWith(Mapping.Key + TEXT("/"), ESearchCase::IgnoreCase) && Mapping.Key.Len() > BestKeyLen) - { - BestKeyLen = Mapping.Key.Len(); - BestDisk = Mapping.Value / VirtualPath.RightChop(Mapping.Key.Len() + 1); - } + return FString(); } - return (BestKeyLen >= 0) ? FPaths::ConvertRelativePathToFull(BestDisk) : FString(); + return FPaths::ConvertRelativePathToFull(Disk); } static void OpenInVSCode(TWeakObjectPtr MICWeak) diff --git a/Source/UShaderLabEditor/UShaderLabEditor.Build.cs b/Source/UShaderLabEditor/UShaderLabEditor.Build.cs index 2edd699..4760a72 100644 --- a/Source/UShaderLabEditor/UShaderLabEditor.Build.cs +++ b/Source/UShaderLabEditor/UShaderLabEditor.Build.cs @@ -20,6 +20,7 @@ public class UShaderLabEditor : ModuleRules PrivateDependencyModuleNames.AddRange(new string[] { "UShaderLab", + "UShaderLabBuilder", "RenderCore", "RHI", "Json", diff --git a/UShaderLab.uplugin b/UShaderLab.uplugin index e58fce0..d79aaf9 100644 --- a/UShaderLab.uplugin +++ b/UShaderLab.uplugin @@ -19,6 +19,11 @@ "Type": "Runtime", "LoadingPhase": "Default" }, + { + "Name": "UShaderLabBuilder", + "Type": "UncookedOnly", + "LoadingPhase": "Default" + }, { "Name": "UShaderLabEditor", "Type": "Editor",