// 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/CoreDelegates.h" #include "Misc/EngineVersionComparison.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" #include "ShaderCore.h" #include "ShaderLabGraphBuilder.h" #include "ShaderLabIDEPrepare.h" #include "ShaderLabMaterialInstanceConstant.h" #include "ShaderLabMaterialRegistry.h" #include "ShaderLabModel.h" #include "UObject/UObjectIterator.h" // FCoreDelegates::OnPostEngineInit became the accessor GetOnPostEngineInit() in UE 5.8; 5.7 exposes the // delegate member directly. Mirror the same bridge the runtime subsystem uses so the shared plugin builds // on both engines. #if UE_VERSION_OLDER_THAN(5, 8, 0) #define SHADERLAB_ON_POST_ENGINE_INIT FCoreDelegates::OnPostEngineInit #else #define SHADERLAB_ON_POST_ENGINE_INIT FCoreDelegates::GetOnPostEngineInit() #endif 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); // First-launch convenience: auto-generate the .usl IDE authoring assets if the project is missing them // (a fresh clone lacks .vscode — it is git-ignored). Deferred to post-engine-init so shader-source // mappings (added above) and UMaterial reflection are ready; the callback self-gates to an interactive // editor session (no cook / -game / commandlet / unattended). PostEngineInitHandle = SHADERLAB_ON_POST_ENGINE_INIT.AddStatic(&ShaderLabIDE_EnsureGeneratedIfMissing); UE_LOG(LogShaderLabBuilder, Log, TEXT("ShaderLabBuilder module started.")); } virtual void ShutdownModule() override { if (BuildHandle.IsValid()) { FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle); BuildHandle.Reset(); } if (PostEngineInitHandle.IsValid()) { SHADERLAB_ON_POST_ENGINE_INIT.Remove(PostEngineInitHandle); PostEngineInitHandle.Reset(); } } private: FDelegateHandle BuildHandle; FDelegateHandle PostEngineInitHandle; }; IMPLEMENT_MODULE(FShaderLabBuilderModule, UShaderLabBuilder)