Add SL_FUNCTION and .uslfunc

This commit is contained in:
Eragon-Brisingr
2026-07-02 17:32:03 +08:00
parent c27af6af49
commit 2ed659d4d2
10 changed files with 1558 additions and 126 deletions

View File

@@ -12,10 +12,13 @@
#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 "ShaderLabMaterialRegistry.h"
#include "ShaderLabModel.h"
#include "ShaderLabParser.h"
#include "ShaderLabSubsystem.h"
#include "ShaderLabVSCodeButton.h"
#include "UObject/Package.h"
@@ -191,6 +194,78 @@ 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<FString, FString>& 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);
N.ReplaceInline(TEXT("\\"), TEXT("/"));
return N;
}
/**
* Every discovered `.usl` shader whose transitive `.uslfunc` import closure contains ChangedDiskPath —
* i.e. the shaders that must be rebuilt when that library changes. Resolves each shader's imports so
* both direct and nested (library-imports-library) dependencies are caught.
*/
static TArray<FString> FindShadersImporting(const FString& ChangedDiskPathNormalized)
{
TArray<FString> Result;
for (const FString& Usl : FShaderLabDiscovery::FindShaderLabFiles())
{
FString Source;
if (!FFileHelper::LoadFileToString(Source, *Usl))
{
continue;
}
FShaderLabModel Model;
TArray<FShaderLabParseError> ParseErrors;
if (!FShaderLabParser::Parse(Source, Usl, Model, ParseErrors) || Model.Imports.Num() == 0)
{
continue;
}
FShaderLabImportResolver::FSourceLoader Loader =
[](const FString& VPath, FString& OutSrc, FString& OutDisk, FString& OutErr) -> bool
{
if (!ResolveVirtualToDisk(VPath, OutDisk)) { OutErr = TEXT("unmapped"); return false; }
if (!FFileHelper::LoadFileToString(OutSrc, *OutDisk)) { OutErr = TEXT("read failed"); return false; }
return true;
};
FShaderLabResolvedProgram Program;
TArray<FString> ResolveErrors;
if (!FShaderLabImportResolver::Resolve(Model, Loader, Program, ResolveErrors))
{
continue;
}
for (const FShaderLabResolvedLibrary& Lib : Program.Libraries)
{
if (NormalizePath(Lib.Model.SourceFilePath) == ChangedDiskPathNormalized)
{
Result.Add(Usl);
break;
}
}
}
return Result;
}
static void OnDirectoryChanged(const TArray<FFileChangeData>& Changes)
{
UShaderLabSubsystem* Subsystem = GEngine ? GEngine->GetEngineSubsystem<UShaderLabSubsystem>() : nullptr;
@@ -199,24 +274,35 @@ private:
return;
}
TSet<FString> Rebuilt;
auto RebuildOnce = [&](const FString& Normalized)
{
if (Rebuilt.Contains(Normalized)) { return; }
Rebuilt.Add(Normalized);
Subsystem->RebuildFromFile(Normalized);
UE_LOG(LogShaderLabEditor, Log, TEXT("ShaderLab hot-reloaded '%s'"), *Normalized);
};
for (const FFileChangeData& Change : Changes)
{
if (!Change.Filename.EndsWith(TEXT(".usl")))
{
continue;
}
if (Change.Action == FFileChangeData::FCA_Removed)
{
continue; // Leave the existing in-memory material in place on delete.
}
const FString Normalized = FPaths::ConvertRelativePathToFull(Change.Filename);
if (Rebuilt.Contains(Normalized))
const FString Ext = FPaths::GetExtension(Change.Filename);
const FString Normalized = NormalizePath(Change.Filename);
if (Ext.Equals(TEXT("usl"), ESearchCase::IgnoreCase))
{
continue;
RebuildOnce(Normalized);
}
else if (Ext.Equals(TEXT("uslfunc"), ESearchCase::IgnoreCase))
{
// A library changed: rebuild every shader that (transitively) imports it, so their promoted
// parameters and emitted function HLSL refresh — the dependency-aware hot reload.
for (const FString& Dependent : FindShadersImporting(Normalized))
{
RebuildOnce(NormalizePath(Dependent));
}
}
Rebuilt.Add(Normalized);
Subsystem->RebuildFromFile(Normalized);
UE_LOG(LogShaderLabEditor, Log, TEXT("ShaderLab hot-reloaded '%s'"), *Normalized);
}
}