mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 06:44:35 +00:00
Add SL_FUNCTION and .uslfunc
This commit is contained in:
@@ -25,6 +25,19 @@
|
||||
// Scalar/Color/Vector defaults come from the HLSL initializer; texture default from DefaultTexture.
|
||||
#define SL_PROPERTY(...)
|
||||
|
||||
// Function-library import. Annotates the `#include "...uslfunc"` on the NEXT line, carrying options such
|
||||
// as Namespace= (prefixes the promoted parameters so same-named properties across libraries stay distinct).
|
||||
// A plain `#include "...uslfunc"` (no marker) imports with default by-name merging.
|
||||
// SL_IMPORT(Namespace = "Detail")
|
||||
// #include "/Project/Lib/DetailLib.uslfunc"
|
||||
#define SL_IMPORT(...)
|
||||
|
||||
// Library function (`.uslfunc` files only). Precedes a normal HLSL function; it may use UE_ intrinsics,
|
||||
// reference the library's own properties, and call other library functions.
|
||||
// SL_FUNCTION()
|
||||
// float3 ApplyDetail(float3 baseColor, float2 uv) { ... }
|
||||
#define SL_FUNCTION(...)
|
||||
|
||||
// Pixel/vertex entry points. Precede a `void Name(inout F... X) { ... }` (Vertex takes FShaderLabVertex).
|
||||
#define SL_SURFACE(...)
|
||||
#define SL_POSTPROCESS(...)
|
||||
|
||||
@@ -30,7 +30,16 @@ TArray<FString> FShaderLabDiscovery::FindShaderLabFiles()
|
||||
{
|
||||
TArray<FString> Found;
|
||||
IFileManager::Get().FindFilesRecursive(Found, *Root, TEXT("*.usl"), true, false, false);
|
||||
Files.Append(Found);
|
||||
for (const FString& File : Found)
|
||||
{
|
||||
// A `.usl` is a renderable shader (registered as a base material); a `.uslfunc` is a function
|
||||
// library (imported by shaders, never a base material). The Windows `*.usl` wildcard also
|
||||
// matches `*.uslfunc` via legacy short-name behavior, so filter by exact extension here.
|
||||
if (FPaths::GetExtension(File).Equals(TEXT("usl"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
Files.Add(File);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Files;
|
||||
|
||||
281
Source/UShaderLab/Private/ShaderLabImportResolver.cpp
Normal file
281
Source/UShaderLab/Private/ShaderLabImportResolver.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "ShaderLabImportResolver.h"
|
||||
|
||||
#include "ShaderLabParser.h"
|
||||
|
||||
const FShaderLabFunction* FShaderLabResolvedProgram::FindFunction(FName Name, int32& OutLibraryIndex) const
|
||||
{
|
||||
if (const int32* LibIdx = FunctionOwnerLibrary.Find(Name))
|
||||
{
|
||||
OutLibraryIndex = *LibIdx;
|
||||
return Libraries[*LibIdx].Model.FindFunction(Name);
|
||||
}
|
||||
OutLibraryIndex = INDEX_NONE;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FName FShaderLabImportResolver::PromoteName(const FString& Namespace, const FShaderLabProperty& Prop)
|
||||
{
|
||||
// StaticBool promotes by bare name regardless of namespace: it reaches bodies through a global
|
||||
// preprocessor `#define` (via the ParameterAnchor), which has no namespace to key on.
|
||||
if (Prop.Type == EShaderLabPropertyType::StaticBool || Namespace.IsEmpty())
|
||||
{
|
||||
return Prop.Name;
|
||||
}
|
||||
return FName(*(Namespace + TEXT("_") + Prop.Name.ToString()));
|
||||
}
|
||||
|
||||
namespace ShaderLabResolver_Private
|
||||
{
|
||||
/** Canonical dedup key for a virtual path (case-insensitive, forward-slashed). */
|
||||
static FString Canonicalize(const FString& Path)
|
||||
{
|
||||
FString P = Path;
|
||||
P.ReplaceInline(TEXT("\\"), TEXT("/"));
|
||||
return P.ToLower();
|
||||
}
|
||||
|
||||
static bool IsTexture(const FShaderLabProperty& P)
|
||||
{
|
||||
return P.Type == EShaderLabPropertyType::Texture2D || P.Type == EShaderLabPropertyType::TextureCube;
|
||||
}
|
||||
|
||||
struct FResolveState
|
||||
{
|
||||
const FShaderLabImportResolver::FSourceLoader& Loader;
|
||||
FShaderLabResolvedProgram& Program;
|
||||
TArray<FString>& Errors;
|
||||
|
||||
/** Canonical path -> resolved library index. */
|
||||
TMap<FString, int32> ByCanonical;
|
||||
/** Canonical paths on the current DFS path (cycle detection). */
|
||||
TArray<FString> VisitingStack;
|
||||
/** Per library index: the direct child (imported) library indices. Index-aligned with Program.Libraries. */
|
||||
TArray<TArray<int32>> ChildLibs;
|
||||
|
||||
void Fail(const FString& Message) { Errors.Add(Message); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Depth-first resolve one library import. Returns its library index, or INDEX_NONE on error.
|
||||
* Post-order: children are appended to Program.Libraries before their importer, so a library always
|
||||
* precedes anything that imports it (emit order = defined-before-use).
|
||||
*/
|
||||
static int32 ResolveLibrary(FResolveState& State, const FString& VirtualPath, const FString& Namespace)
|
||||
{
|
||||
const FString Canonical = Canonicalize(VirtualPath);
|
||||
|
||||
if (State.VisitingStack.Contains(Canonical))
|
||||
{
|
||||
State.Fail(FString::Printf(TEXT("ShaderLab: import cycle detected at '%s'"), *VirtualPath));
|
||||
return INDEX_NONE;
|
||||
}
|
||||
if (const int32* Existing = State.ByCanonical.Find(Canonical))
|
||||
{
|
||||
// Already resolved via another path: the namespace must agree (a library has one identity).
|
||||
const FShaderLabResolvedLibrary& Lib = State.Program.Libraries[*Existing];
|
||||
if (Lib.Namespace != Namespace)
|
||||
{
|
||||
State.Fail(FString::Printf(
|
||||
TEXT("ShaderLab: library '%s' is imported with conflicting namespaces '%s' and '%s'"),
|
||||
*VirtualPath, Lib.Namespace.IsEmpty() ? TEXT("<none>") : *Lib.Namespace,
|
||||
Namespace.IsEmpty() ? TEXT("<none>") : *Namespace));
|
||||
return INDEX_NONE;
|
||||
}
|
||||
return *Existing;
|
||||
}
|
||||
|
||||
FString Source, DiskPath, LoadError;
|
||||
if (!State.Loader(VirtualPath, Source, DiskPath, LoadError))
|
||||
{
|
||||
State.Fail(FString::Printf(TEXT("ShaderLab: cannot load imported library '%s': %s"), *VirtualPath, *LoadError));
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
FShaderLabModel LibModel;
|
||||
TArray<FShaderLabParseError> ParseErrors;
|
||||
// Parse under the disk path so bodies' `#line` directives (and thus compile-error clicks) resolve
|
||||
// to the real file; dedup/diagnostics below still key off the virtual path.
|
||||
if (!FShaderLabParser::Parse(Source, DiskPath.IsEmpty() ? VirtualPath : DiskPath, LibModel, ParseErrors))
|
||||
{
|
||||
for (const FShaderLabParseError& E : ParseErrors)
|
||||
{
|
||||
State.Fail(FString::Printf(TEXT("ShaderLab: parse error in library '%s' %s"), *VirtualPath, *E.ToString()));
|
||||
}
|
||||
return INDEX_NONE;
|
||||
}
|
||||
if (!LibModel.bIsFunctionLibrary)
|
||||
{
|
||||
State.Fail(FString::Printf(TEXT("ShaderLab: '%s' is imported as a function library but is not a `.uslfunc`"), *VirtualPath));
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
// Recurse into this library's own imports first (post-order).
|
||||
State.VisitingStack.Push(Canonical);
|
||||
TArray<int32> Children;
|
||||
for (const FShaderLabImport& Import : LibModel.Imports)
|
||||
{
|
||||
const int32 ChildIdx = ResolveLibrary(State, Import.Path, Import.Namespace);
|
||||
if (ChildIdx == INDEX_NONE)
|
||||
{
|
||||
State.VisitingStack.Pop();
|
||||
return INDEX_NONE;
|
||||
}
|
||||
Children.AddUnique(ChildIdx);
|
||||
}
|
||||
State.VisitingStack.Pop();
|
||||
|
||||
FShaderLabResolvedLibrary Resolved;
|
||||
Resolved.CanonicalPath = Canonical;
|
||||
Resolved.VirtualPath = VirtualPath;
|
||||
Resolved.Namespace = Namespace;
|
||||
Resolved.Model = MoveTemp(LibModel);
|
||||
const int32 Index = State.Program.Libraries.Add(MoveTemp(Resolved));
|
||||
State.ChildLibs.Add(MoveTemp(Children));
|
||||
check(State.ChildLibs.Num() == State.Program.Libraries.Num());
|
||||
State.ByCanonical.Add(Canonical, Index);
|
||||
return Index;
|
||||
}
|
||||
|
||||
/** Accumulate the subtree (self + all transitively imported libraries) of a library, memoized. */
|
||||
static void CollectSubtree(const FResolveState& State, int32 LibIndex, TSet<int32>& OutSet)
|
||||
{
|
||||
if (OutSet.Contains(LibIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
OutSet.Add(LibIndex);
|
||||
for (const int32 Child : State.ChildLibs[LibIndex])
|
||||
{
|
||||
CollectSubtree(State, Child, OutSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FShaderLabImportResolver::Resolve(
|
||||
const FShaderLabModel& RootModel,
|
||||
const FSourceLoader& Loader,
|
||||
FShaderLabResolvedProgram& OutProgram,
|
||||
TArray<FString>& OutErrors)
|
||||
{
|
||||
using namespace ShaderLabResolver_Private;
|
||||
|
||||
OutProgram = FShaderLabResolvedProgram();
|
||||
|
||||
FResolveState State{ Loader, OutProgram, OutErrors };
|
||||
|
||||
// 1) Load the transitive library closure (post-order into OutProgram.Libraries).
|
||||
for (const FShaderLabImport& Import : RootModel.Imports)
|
||||
{
|
||||
if (ResolveLibrary(State, Import.Path, Import.Namespace) == INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Function-name uniqueness across the whole closure (functions are flat HLSL symbols).
|
||||
for (int32 LibIdx = 0; LibIdx < OutProgram.Libraries.Num(); ++LibIdx)
|
||||
{
|
||||
for (const FShaderLabFunction& Fn : OutProgram.Libraries[LibIdx].Model.Functions)
|
||||
{
|
||||
if (const int32* Prev = OutProgram.FunctionOwnerLibrary.Find(Fn.Name))
|
||||
{
|
||||
OutErrors.Add(FString::Printf(
|
||||
TEXT("ShaderLab: function '%s' is declared in both '%s' and '%s' (function names must be unique across imports)"),
|
||||
*Fn.Name.ToString(), *OutProgram.Libraries[*Prev].VirtualPath, *OutProgram.Libraries[LibIdx].VirtualPath));
|
||||
return false;
|
||||
}
|
||||
OutProgram.FunctionOwnerLibrary.Add(Fn.Name, LibIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Promote properties. Root shader properties come first (owner = INDEX_NONE, no namespace); then
|
||||
// each library's properties, namespaced. Dedup by promoted name: same type merges (first wins, so a
|
||||
// shader can override a library default by re-declaring the same name); different type is an error.
|
||||
auto AddPromoted = [&OutProgram, &OutErrors](const FShaderLabProperty& Prop, const FName PromotedName, int32 OwnerLib) -> bool
|
||||
{
|
||||
if (const int32* Existing = OutProgram.PropertyOwner.Find(PromotedName))
|
||||
{
|
||||
const FShaderLabProperty& Prior = OutProgram.Properties[OutProgram.Properties.IndexOfByPredicate(
|
||||
[PromotedName](const FShaderLabProperty& P) { return P.Name == PromotedName; })];
|
||||
if (Prior.Type != Prop.Type)
|
||||
{
|
||||
OutErrors.Add(FString::Printf(
|
||||
TEXT("ShaderLab: parameter '%s' is declared with conflicting types across the shader/imported libraries"),
|
||||
*PromotedName.ToString()));
|
||||
return false;
|
||||
}
|
||||
return true; // Same type: keep the first (shader-wins) declaration.
|
||||
}
|
||||
FShaderLabProperty Promoted = Prop;
|
||||
Promoted.Name = PromotedName;
|
||||
OutProgram.Properties.Add(MoveTemp(Promoted));
|
||||
OutProgram.PropertyOwner.Add(PromotedName, OwnerLib);
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const FShaderLabProperty& Prop : RootModel.Properties)
|
||||
{
|
||||
if (!AddPromoted(Prop, Prop.Name, INDEX_NONE))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int32 LibIdx = 0; LibIdx < OutProgram.Libraries.Num(); ++LibIdx)
|
||||
{
|
||||
const FShaderLabResolvedLibrary& Lib = OutProgram.Libraries[LibIdx];
|
||||
for (const FShaderLabProperty& Prop : Lib.Model.Properties)
|
||||
{
|
||||
if (!AddPromoted(Prop, PromoteName(Lib.Namespace, Prop), LibIdx))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Build each library's PropRewrite (bare identifier -> promoted name) over its visibility scope
|
||||
// (own + transitively imported properties), so function bodies can be rewritten. A bare name that
|
||||
// is visible from two libraries with different promoted names is ambiguous HLSL -> error.
|
||||
for (int32 LibIdx = 0; LibIdx < OutProgram.Libraries.Num(); ++LibIdx)
|
||||
{
|
||||
TSet<int32> Subtree;
|
||||
CollectSubtree(State, LibIdx, Subtree);
|
||||
|
||||
FShaderLabResolvedLibrary& Lib = OutProgram.Libraries[LibIdx];
|
||||
for (const int32 VisibleIdx : Subtree)
|
||||
{
|
||||
const FShaderLabResolvedLibrary& Visible = OutProgram.Libraries[VisibleIdx];
|
||||
for (const FShaderLabProperty& Prop : Visible.Model.Properties)
|
||||
{
|
||||
// StaticBool reaches function bodies through the global `#define` (promoted == bare name), so
|
||||
// it is never a rewritten identifier nor a function parameter — keep it out of PropRewrite.
|
||||
if (Prop.Type == EShaderLabPropertyType::StaticBool)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FName Promoted = PromoteName(Visible.Namespace, Prop);
|
||||
if (const FName* Prior = Lib.PropRewrite.Find(Prop.Name))
|
||||
{
|
||||
if (*Prior != Promoted)
|
||||
{
|
||||
OutErrors.Add(FString::Printf(
|
||||
TEXT("ShaderLab: property '%s' is visible from multiple libraries in scope of '%s' — rename or namespace one of them"),
|
||||
*Prop.Name.ToString(), *Lib.VirtualPath));
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Lib.PropRewrite.Add(Prop.Name, Promoted);
|
||||
if (IsTexture(Prop))
|
||||
{
|
||||
Lib.PropRewrite.Add(
|
||||
FName(*(Prop.Name.ToString() + TEXT("Sampler"))),
|
||||
FName(*(Promoted.ToString() + TEXT("Sampler"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -12,6 +12,11 @@ const FShaderLabInterpolator* FShaderLabModel::FindInterpolator(FName InName) co
|
||||
return Interpolators.FindByPredicate([InName](const FShaderLabInterpolator& I) { return I.Name == InName; });
|
||||
}
|
||||
|
||||
const FShaderLabFunction* FShaderLabModel::FindFunction(FName InName) const
|
||||
{
|
||||
return Functions.FindByPredicate([InName](const FShaderLabFunction& F) { return F.Name == InName; });
|
||||
}
|
||||
|
||||
bool FShaderLabModel::HasStaticSwitches() const
|
||||
{
|
||||
return Properties.ContainsByPredicate(
|
||||
|
||||
@@ -786,22 +786,132 @@ namespace
|
||||
Model.Properties.Add(MoveTemp(Prop));
|
||||
}
|
||||
|
||||
void ParseInclude(FScanner& S, FShaderLabModel& Model)
|
||||
/**
|
||||
* At a '#include' directive ('#'+'include' already consumed). A `.uslfunc` path (or any include
|
||||
* annotated by a preceding SL_IMPORT) is recorded as a function-library import; other quoted paths are
|
||||
* real `.ush` library dependencies. bIsImport carries an explicit SL_IMPORT annotation (with Namespace).
|
||||
*/
|
||||
void ParseInclude(FScanner& S, FShaderLabModel& Model, bool bIsImport, const FString& Namespace, int32 ImportLine)
|
||||
{
|
||||
// At a '#include' directive; '#' already consumed, 'include' identifier already read.
|
||||
FString Path;
|
||||
if (!S.ReadString(Path))
|
||||
{
|
||||
if (bIsImport)
|
||||
{
|
||||
S.Error(TEXT("SL_IMPORT must annotate a `#include \"...uslfunc\"` directive"));
|
||||
return;
|
||||
}
|
||||
// Tolerate angle-bracket includes by skipping the line; ShaderLab uses quoted virtual paths.
|
||||
S.SkipToEndOfLine();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool bUslFunc = Path.EndsWith(TEXT(".uslfunc"), ESearchCase::IgnoreCase);
|
||||
if (bIsImport || bUslFunc)
|
||||
{
|
||||
if (!bUslFunc)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("SL_IMPORT expects a `.uslfunc` library path, got '%s'"), *Path));
|
||||
return;
|
||||
}
|
||||
FShaderLabImport Import;
|
||||
Import.Path = Path;
|
||||
Import.Namespace = Namespace;
|
||||
Import.Line = ImportLine > 0 ? ImportLine : S.Line;
|
||||
Model.Imports.Add(MoveTemp(Import));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAuthoringShimInclude(Path))
|
||||
{
|
||||
Model.Includes.Add(Path);
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse `SL_IMPORT( [Namespace = "..."] )` options; fills OutNamespace. Returns false on error. */
|
||||
bool ParseImport(FScanner& S, FString& OutNamespace)
|
||||
{
|
||||
const int32 SpecLine = S.Line, SpecCol = S.Column;
|
||||
FString Inner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const FString& StmtRaw : SplitTopLevel(Inner, TEXT(',')))
|
||||
{
|
||||
const FString Stmt = StmtRaw.TrimStartAndEnd();
|
||||
if (Stmt.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FString Key, Value;
|
||||
if (!Stmt.Split(TEXT("="), &Key, &Value))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Malformed SL_IMPORT option '%s' (expected Key = Value)"), *Stmt), SpecLine, SpecCol);
|
||||
return false;
|
||||
}
|
||||
Key = Key.TrimStartAndEnd();
|
||||
Value = Value.TrimStartAndEnd().TrimQuotes();
|
||||
if (Key == TEXT("Namespace"))
|
||||
{
|
||||
OutNamespace = Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Unknown SL_IMPORT option '%s'"), *Key), SpecLine, SpecCol);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** SL_FUNCTION() <ret> <Name>(<params>) { <body> } — a reusable library function (`.uslfunc` only). */
|
||||
void ParseFunction(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
FString Ignored;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Ignored)) // SL_FUNCTION()
|
||||
{
|
||||
return;
|
||||
}
|
||||
S.SkipTrivia();
|
||||
const int32 DeclLine = S.Line, DeclCol = S.Column;
|
||||
|
||||
FString RetType;
|
||||
if (!S.ReadIdentifier(RetType))
|
||||
{
|
||||
S.Error(TEXT("Expected a function declaration after SL_FUNCTION"));
|
||||
return;
|
||||
}
|
||||
FString Name, SigInner, Body;
|
||||
int32 BodyLine = 0;
|
||||
if (!S.ReadIdentifier(Name))
|
||||
{
|
||||
S.Error(TEXT("Expected a function name"));
|
||||
return;
|
||||
}
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), SigInner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Body, &BodyLine))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const FName FnName(*Name);
|
||||
if (Model.Functions.ContainsByPredicate([&FnName](const FShaderLabFunction& E) { return E.Name == FnName; }))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Duplicate function name '%s'"), *Name), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
FShaderLabFunction Fn;
|
||||
Fn.Name = FnName;
|
||||
Fn.ReturnType = RetType;
|
||||
Fn.SignatureInner = MoveTemp(SigInner);
|
||||
Fn.Body = MoveTemp(Body);
|
||||
Fn.BodyLine = BodyLine;
|
||||
Model.Functions.Add(MoveTemp(Fn));
|
||||
}
|
||||
|
||||
void ParseEntry(FScanner& S, FShaderLabModel& Model, const FString& Marker)
|
||||
{
|
||||
// Marker macro takes no args: SL_SURFACE()/SL_POSTPROCESS()/SL_UI()/SL_VERTEX().
|
||||
@@ -1216,9 +1326,18 @@ bool FShaderLabParser::Parse(
|
||||
// Identity + display name come from the file name (e.g. "Basic.usl" -> "Basic"); the file IS the
|
||||
// shader. Annotated-HLSL constructs are parsed flat to EOF in any order.
|
||||
OutModel.ShaderName = FPaths::GetBaseFilename(SourceName);
|
||||
// A `.uslfunc` is a reusable function library (no pixel/vertex entry, no base material); a `.usl` is a
|
||||
// renderable shader. The extension is the single source of truth for which constructs are legal.
|
||||
OutModel.bIsFunctionLibrary = FPaths::GetExtension(SourceName).Equals(TEXT("uslfunc"), ESearchCase::IgnoreCase);
|
||||
const TCHAR* const FileKind = OutModel.bIsFunctionLibrary ? TEXT("function library") : TEXT("shader");
|
||||
|
||||
FScanner S(Source, OutErrors);
|
||||
|
||||
// A pending `SL_IMPORT(...)` annotation must be consumed by the very next `#include "...uslfunc"`.
|
||||
bool bPendingImport = false;
|
||||
FString PendingImportNamespace;
|
||||
int32 PendingImportLine = 0;
|
||||
|
||||
while (!S.bFailed)
|
||||
{
|
||||
S.SkipTrivia();
|
||||
@@ -1227,8 +1346,16 @@ bool FShaderLabParser::Parse(
|
||||
break; // End of file: all constructs parsed.
|
||||
}
|
||||
|
||||
// Top-level preprocessor lines: `#include "..."` feeds Model.Includes; every other preprocessor
|
||||
// line (`#pragma`, `#define`, `#if`, ...) is free HLSL captured verbatim into LocalCode.
|
||||
// Contract: SL_IMPORT annotates exactly the next `#include`. Anything else after it is an error.
|
||||
if (bPendingImport && S.Peek() != TEXT('#'))
|
||||
{
|
||||
S.Error(TEXT("SL_IMPORT must be immediately followed by a `#include \"...uslfunc\"` directive"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Top-level preprocessor lines: `#include "..."` feeds Model.Includes (or Model.Imports for a
|
||||
// `.uslfunc` / SL_IMPORT-annotated include); every other preprocessor line (`#pragma`, `#define`,
|
||||
// `#if`, ...) is free HLSL captured verbatim into LocalCode.
|
||||
if (S.Peek() == TEXT('#'))
|
||||
{
|
||||
const TCHAR* Save = S.Ptr;
|
||||
@@ -1237,7 +1364,15 @@ bool FShaderLabParser::Parse(
|
||||
FString Directive;
|
||||
if (S.ReadIdentifier(Directive) && Directive == TEXT("include"))
|
||||
{
|
||||
ParseInclude(S, OutModel);
|
||||
ParseInclude(S, OutModel, bPendingImport, PendingImportNamespace, PendingImportLine);
|
||||
bPendingImport = false;
|
||||
PendingImportNamespace.Reset();
|
||||
PendingImportLine = 0;
|
||||
}
|
||||
else if (bPendingImport)
|
||||
{
|
||||
S.Error(TEXT("SL_IMPORT must be immediately followed by a `#include \"...uslfunc\"` directive"));
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1257,8 +1392,41 @@ bool FShaderLabParser::Parse(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Token == TEXT("SL_SETTINGS"))
|
||||
// Constructs that only make sense in a renderable shader (a library has no material to render).
|
||||
auto RejectInLibrary = [&](const TCHAR* What) -> bool
|
||||
{
|
||||
if (OutModel.bIsFunctionLibrary)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s is not allowed in a %s"), What, FileKind), DeclSaveLine, DeclSaveCol);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (Token == TEXT("SL_IMPORT"))
|
||||
{
|
||||
const int32 ImportLine = DeclSaveLine;
|
||||
FString Ns;
|
||||
if (!ParseImport(S, Ns))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bPendingImport = true;
|
||||
PendingImportNamespace = MoveTemp(Ns);
|
||||
PendingImportLine = ImportLine;
|
||||
}
|
||||
else if (Token == TEXT("SL_FUNCTION"))
|
||||
{
|
||||
if (!OutModel.bIsFunctionLibrary)
|
||||
{
|
||||
S.Error(TEXT("SL_FUNCTION is only allowed in a `.uslfunc` function library"), DeclSaveLine, DeclSaveCol);
|
||||
return false;
|
||||
}
|
||||
ParseFunction(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_SETTINGS"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_SETTINGS"))) { return false; }
|
||||
ParseSettings(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_PROPERTY"))
|
||||
@@ -1267,30 +1435,37 @@ bool FShaderLabParser::Parse(
|
||||
}
|
||||
else if (Token == TEXT("SL_SURFACE") || Token == TEXT("SL_POSTPROCESS") || Token == TEXT("SL_UI") || Token == TEXT("SL_VERTEX"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("A pixel/vertex entry"))) { return false; }
|
||||
ParseEntry(S, OutModel, Token);
|
||||
}
|
||||
else if (Token == TEXT("SL_SLAB"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_SLAB"))) { return false; }
|
||||
ParseSlab(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_VALUE"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_VALUE"))) { return false; }
|
||||
ParseValue(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_INTERPOLATOR"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_INTERPOLATOR"))) { return false; }
|
||||
ParseInterpolator(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_FRONTMATERIAL"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_FRONTMATERIAL"))) { return false; }
|
||||
ParseFrontMaterial(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_OPACITY"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_OPACITY"))) { return false; }
|
||||
ParseMaterialOutput(S, OutModel.OpacityValueName, TEXT("SL_OPACITY"));
|
||||
}
|
||||
else if (Token == TEXT("SL_OPACITY_MASK"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_OPACITY_MASK"))) { return false; }
|
||||
ParseMaterialOutput(S, OutModel.OpacityMaskValueName, TEXT("SL_OPACITY_MASK"));
|
||||
}
|
||||
else
|
||||
@@ -1311,6 +1486,19 @@ bool FShaderLabParser::Parse(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bPendingImport)
|
||||
{
|
||||
S.Error(TEXT("SL_IMPORT at end of file has no `#include \"...uslfunc\"` to annotate"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// A function library has no pixel/vertex stage: skip all entry-point validation. Its Properties,
|
||||
// Functions, Includes, Imports and LocalCode are the whole payload.
|
||||
if (OutModel.bIsFunctionLibrary)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pixel stage: exactly one of { single Surface } or { named Slabs + FrontMaterial }.
|
||||
const bool bHasMultiSlab = OutModel.Slabs.Num() > 0 || OutModel.TopologyRoot != INDEX_NONE;
|
||||
if (OutModel.bHasSurface && bHasMultiSlab)
|
||||
|
||||
85
Source/UShaderLab/Public/ShaderLabImportResolver.h
Normal file
85
Source/UShaderLab/Public/ShaderLabImportResolver.h
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "ShaderLabModel.h"
|
||||
|
||||
/**
|
||||
* Resolves the transitive `.uslfunc` import closure of a shader (or library) model: loads + parses each
|
||||
* imported library, detects cycles / namespace conflicts / duplicate function names, and promotes every
|
||||
* library's properties onto the consuming material (namespaced so same-named properties across libraries
|
||||
* stay distinct).
|
||||
*
|
||||
* Pure model logic (no Engine/GPU dependency): the file bytes come in through an injected source loader,
|
||||
* so this is unit-testable with in-memory sources and reused by the editor graph builder with a
|
||||
* disk-backed loader. Promotion is an editor/cook-build concern (the cooked runtime never resolves
|
||||
* imports — the MIC's baked shader map already carries the promoted parameters).
|
||||
*/
|
||||
|
||||
/** One library in the resolved closure. */
|
||||
struct USHADERLAB_API FShaderLabResolvedLibrary
|
||||
{
|
||||
/** Canonical (lower-cased, forward-slashed) virtual path, used as the dedup key. */
|
||||
FString CanonicalPath;
|
||||
/** Virtual path as first imported (for diagnostics / #line paths). */
|
||||
FString VirtualPath;
|
||||
/** Effective promotion namespace ("" = merge promoted properties by bare name). */
|
||||
FString Namespace;
|
||||
/** The parsed library model (its Functions/Includes/LocalCode are emitted by the graph builder). */
|
||||
FShaderLabModel Model;
|
||||
|
||||
/**
|
||||
* For every property visible in THIS library file (its own + all transitively imported libraries'),
|
||||
* maps the bare in-HLSL identifier to the promoted material-parameter name that a function body's
|
||||
* reference must be rewritten to. Texture properties also map the `<Name>Sampler` alias. StaticBool
|
||||
* properties map to their bare name (they promote by name and reach bodies via the global `#define`).
|
||||
*/
|
||||
TMap<FName, FName> PropRewrite;
|
||||
};
|
||||
|
||||
struct USHADERLAB_API FShaderLabResolvedProgram
|
||||
{
|
||||
/**
|
||||
* Final material-parameter set: the root shader's own properties first, then every library's promoted
|
||||
* properties (namespaced, deduped by promoted name). This replaces Model.Properties for parameter-node
|
||||
* creation in the graph builder.
|
||||
*/
|
||||
TArray<FShaderLabProperty> Properties;
|
||||
|
||||
/** Libraries in dependency order (a library appears before any library that imports it). */
|
||||
TArray<FShaderLabResolvedLibrary> Libraries;
|
||||
|
||||
/** Promoted-property name -> the source library index it came from (INDEX_NONE = the root shader). */
|
||||
TMap<FName, int32> PropertyOwner;
|
||||
|
||||
/** Function name -> owning library index (functions are unique across the whole closure). */
|
||||
TMap<FName, int32> FunctionOwnerLibrary;
|
||||
|
||||
/** Find a library function by name across the closure; returns nullptr (and INDEX_NONE) if absent. */
|
||||
const FShaderLabFunction* FindFunction(FName Name, int32& OutLibraryIndex) const;
|
||||
};
|
||||
|
||||
class USHADERLAB_API FShaderLabImportResolver
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Load the source text for a virtual `.uslfunc` path. Returns true and fills OutSource (and OutDiskPath,
|
||||
* the absolute on-disk path used for `#line` error mapping) on success; on failure returns false and
|
||||
* fills OutError. In tests the disk path may simply echo the virtual path.
|
||||
*/
|
||||
using FSourceLoader = TFunction<bool(const FString& /*VirtualPath*/, FString& /*OutSource*/, FString& /*OutDiskPath*/, FString& /*OutError*/)>;
|
||||
|
||||
/**
|
||||
* Resolve RootModel's transitive import closure. Returns true on success; on failure OutErrors is
|
||||
* non-empty (each message is already prefixed for the shader-compile error path where relevant).
|
||||
*/
|
||||
static bool Resolve(
|
||||
const FShaderLabModel& RootModel,
|
||||
const FSourceLoader& Loader,
|
||||
FShaderLabResolvedProgram& OutProgram,
|
||||
TArray<FString>& OutErrors);
|
||||
|
||||
/** The promoted material-parameter name for a property under a namespace (StaticBool ignores namespace). */
|
||||
static FName PromoteName(const FString& Namespace, const FShaderLabProperty& Prop);
|
||||
};
|
||||
@@ -124,6 +124,39 @@ struct USHADERLAB_API FShaderLabLocalCode
|
||||
int32 Line = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* A `SL_FUNCTION() <ret> <Name>(<params>){...}` block in a `.uslfunc` function library. A library
|
||||
* function is reusable HLSL that may use `UE_` intrinsics, reference the library's own properties, and
|
||||
* call other library functions. The graph builder emits it (rewritten to take the referenced
|
||||
* properties/intrinsics as trailing parameters) and injects the matching context at each call site.
|
||||
*/
|
||||
struct USHADERLAB_API FShaderLabFunction
|
||||
{
|
||||
FName Name;
|
||||
/** HLSL return type verbatim ("float3", "void", ...). */
|
||||
FString ReturnType;
|
||||
/** Author-declared parameters verbatim (the text inside the signature `(...)`). */
|
||||
FString SignatureInner;
|
||||
FString Body;
|
||||
int32 BodyLine = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* A `#include "<path>.uslfunc"` import of a function library, optionally annotated by a preceding
|
||||
* `SL_IMPORT(Namespace = "...")` marker. Importing a library promotes its properties onto the consuming
|
||||
* material and makes its functions callable. Namespacing decouples the promoted material-parameter name
|
||||
* (identity/merge key) from the in-HLSL identifier, so same-named properties across libraries stay
|
||||
* distinct. Present in both shaders and libraries (libraries may import other libraries).
|
||||
*/
|
||||
struct USHADERLAB_API FShaderLabImport
|
||||
{
|
||||
/** Virtual path as written, e.g. "/Project/Lib/DetailLib.uslfunc". */
|
||||
FString Path;
|
||||
/** From `SL_IMPORT(Namespace = "...")`; empty means merge promoted properties by bare name. */
|
||||
FString Namespace;
|
||||
int32 Line = 0;
|
||||
};
|
||||
|
||||
/** Substrate topology operators (short DSL aliases mapping to engine Substrate expression nodes). */
|
||||
enum class EShaderLabOp : uint8
|
||||
{
|
||||
@@ -196,6 +229,24 @@ struct USHADERLAB_API FShaderLabModel
|
||||
TArray<FShaderLabProperty> Properties;
|
||||
TArray<FString> Includes;
|
||||
|
||||
/**
|
||||
* `.uslfunc` function-library imports (`#include "...uslfunc"`, optionally annotated by SL_IMPORT).
|
||||
* Present in both shaders and libraries. Resolved (transitively) by the editor graph builder, which
|
||||
* promotes each library's properties and emits its functions; the cooked runtime ignores them (the
|
||||
* MIC's baked shader map already carries the promoted parameters).
|
||||
*/
|
||||
TArray<FShaderLabImport> Imports;
|
||||
|
||||
/**
|
||||
* True when this model was parsed from a `.uslfunc`: it is a reusable function library, NOT a renderable
|
||||
* shader. It has no pixel/vertex entry and no base material is created for it; only its Properties,
|
||||
* Functions, Includes, Imports and LocalCode are meaningful.
|
||||
*/
|
||||
bool bIsFunctionLibrary = false;
|
||||
|
||||
/** Reusable functions (only in a `.uslfunc` library). */
|
||||
TArray<FShaderLabFunction> Functions;
|
||||
|
||||
// Pixel stage. Either a single `Surface(...)` (sugar: one anonymous slab straight to FrontMaterial),
|
||||
// OR one-or-more named `Slab` blocks + a `FrontMaterial = <topology>` expression. The two are
|
||||
// mutually exclusive (enforced by the parser).
|
||||
@@ -235,6 +286,9 @@ struct USHADERLAB_API FShaderLabModel
|
||||
/** Find an interpolator by name (nullptr if absent). */
|
||||
const FShaderLabInterpolator* FindInterpolator(FName InName) const;
|
||||
|
||||
/** Find a library function by name (nullptr if absent). */
|
||||
const FShaderLabFunction* FindFunction(FName InName) const;
|
||||
|
||||
/** True if any property is a StaticBool (drives shader-map permutation count). */
|
||||
bool HasStaticSwitches() const;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
// generated from FShaderLabIntrinsicRegistry. (The static
|
||||
// ShaderLab.ush umbrella includes it; it is IDE-only.)
|
||||
// 2. <Project>/.vscode/settings.json — merges (non-destructively) files.associations
|
||||
// (*.usl/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping
|
||||
// (*.usl/*.uslfunc/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping
|
||||
// (virtual shader roots -> disk, from AllShaderSourceDirectoryMappings).
|
||||
// pathRemapping is machine-specific; the command regenerates it.
|
||||
|
||||
@@ -338,8 +338,8 @@ namespace ShaderLabIDEPrepare_Private
|
||||
return Remap;
|
||||
}
|
||||
|
||||
// Merge files.associations (*.usl/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping into
|
||||
// .vscode/settings.json, preserving all other keys. Rewrites only when something actually changed.
|
||||
// Merge files.associations (*.usl/*.uslfunc/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping
|
||||
// into .vscode/settings.json, preserving all other keys. Rewrites only when something actually changed.
|
||||
static void MergeVSCodeSettings(const FString& ProjectDir)
|
||||
{
|
||||
const FString SettingsPath = FPaths::Combine(ProjectDir, TEXT(".vscode"), TEXT("settings.json"));
|
||||
@@ -368,7 +368,7 @@ namespace ShaderLabIDEPrepare_Private
|
||||
const TSharedPtr<FJsonObject>* AssocPtr = nullptr;
|
||||
const TSharedPtr<FJsonObject> Assoc = Settings->TryGetObjectField(TEXT("files.associations"), AssocPtr)
|
||||
? *AssocPtr : MakeShared<FJsonObject>();
|
||||
for (const TCHAR* Ext : { TEXT("*.usl"), TEXT("*.usf"), TEXT("*.ush") })
|
||||
for (const TCHAR* Ext : { TEXT("*.usl"), TEXT("*.uslfunc"), TEXT("*.usf"), TEXT("*.ush") })
|
||||
{
|
||||
FString Value;
|
||||
if (!Assoc->TryGetStringField(Ext, Value) || Value != TEXT("hlsl"))
|
||||
|
||||
Reference in New Issue
Block a user