mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 06:44:35 +00:00
182 lines
8.1 KiB
C++
182 lines
8.1 KiB
C++
// 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/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"
|
|
|
|
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<FString> 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<UShaderLabMaterialInstanceConstant> 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<IPlugin> 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/<Name>.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 = FCoreDelegates::GetOnPostEngineInit().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())
|
|
{
|
|
FCoreDelegates::GetOnPostEngineInit().Remove(PostEngineInitHandle);
|
|
PostEngineInitHandle.Reset();
|
|
}
|
|
}
|
|
|
|
private:
|
|
FDelegateHandle BuildHandle;
|
|
FDelegateHandle PostEngineInitHandle;
|
|
};
|
|
|
|
IMPLEMENT_MODULE(FShaderLabBuilderModule, UShaderLabBuilder)
|