mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 23:04:37 +00:00
Enhance VSCode coding (Use shader-validator VSCode plugin
This commit is contained in:
@@ -2,19 +2,20 @@
|
||||
//
|
||||
// `ShaderLab.IDE.Prepare` console command — generates the IDE authoring assets so a `.usl` file
|
||||
// (which is plain HLSL plus SL_* annotation macros) gets full member/function/include completion
|
||||
// from Rider's UE shader support or VSCode + HLSL Tools. It writes, all from a single source of truth:
|
||||
// 1. <Plugin>/Shaders/ShaderLab.ush — umbrella header: DSL macros + structs + `namespace UE`
|
||||
// intrinsic stubs generated from FShaderLabIntrinsicRegistry.
|
||||
// 2. <Project>/shadertoolsconfig.json — HLSL Tools virtual-directory mappings, dumped from
|
||||
// AllShaderSourceDirectoryMappings() (the same table the
|
||||
// runtime registers). Machine-specific → gitignored.
|
||||
// 3. <Project>/.vscode/settings.json — merges *.usl/*.usf/*.ush -> hlsl into files.associations
|
||||
// (non-destructive: existing keys are preserved).
|
||||
// from Rider's UE shader support or VSCode + the shader-validator extension. It writes:
|
||||
// 1. <Plugin>/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. <Project>/.vscode/settings.json — merges (non-destructively) files.associations
|
||||
// (*.usl/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping
|
||||
// (virtual shader roots -> disk, from AllShaderSourceDirectoryMappings).
|
||||
// pathRemapping is machine-specific; the command regenerates it.
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "UObject/Class.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
@@ -104,8 +105,12 @@ namespace ShaderLabIDEPrepare_Private
|
||||
return Out;
|
||||
}
|
||||
|
||||
/** Build the `namespace UE { ... }` block of stub declarations from the registry (sorted, deterministic). */
|
||||
static FString GenerateIntrinsicNamespace()
|
||||
/**
|
||||
* Build the `UE_Name(...)` stub declarations from the registry (sorted, deterministic). They are
|
||||
* free functions (not a `namespace UE`) because shader-validator's HLSL parser has no namespace
|
||||
* support; the intrinsics are written `UE_Name(...)` in .usl and matched by that prefix.
|
||||
*/
|
||||
static FString GenerateIntrinsicStubs()
|
||||
{
|
||||
TArray<FShaderLabIntrinsicDesc> Descs;
|
||||
FShaderLabIntrinsicRegistry::Get().ForEach([&Descs](const FShaderLabIntrinsicDesc& D) { Descs.Add(D); });
|
||||
@@ -115,27 +120,61 @@ namespace ShaderLabIDEPrepare_Private
|
||||
});
|
||||
|
||||
FString Body;
|
||||
Body += TEXT("namespace UE\n{\n");
|
||||
|
||||
// Emit each reflected enum used by an intrinsic arg once, as a real HLSL enum, so the param is
|
||||
// typed and its tokens complete (`UE_ViewProperty(MEVP_FieldOfView)`). The graph builder resolves
|
||||
// the token by name, so the enum's numeric values are irrelevant.
|
||||
{
|
||||
TSet<FString> EmittedEnums;
|
||||
FString Enums;
|
||||
for (const FShaderLabIntrinsicDesc& D : Descs)
|
||||
{
|
||||
for (const FShaderLabIntrinsicParam& P : D.Params)
|
||||
{
|
||||
if (!P.Enum || EmittedEnums.Contains(P.Enum->GetName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EmittedEnums.Add(P.Enum->GetName());
|
||||
Enums += FString::Printf(TEXT("enum %s\n{\n"), *P.Enum->GetName());
|
||||
for (int32 Index = 0; Index < P.Enum->NumEnums(); ++Index)
|
||||
{
|
||||
const FString Token = P.Enum->GetNameStringByIndex(Index);
|
||||
if (!Token.IsEmpty() && !Token.EndsWith(TEXT("_MAX")))
|
||||
{
|
||||
Enums += FString::Printf(TEXT("\t%s,\n"), *Token);
|
||||
}
|
||||
}
|
||||
Enums += TEXT("};\n");
|
||||
}
|
||||
}
|
||||
if (!Enums.IsEmpty())
|
||||
{
|
||||
Body += Enums;
|
||||
Body += TEXT("\n");
|
||||
}
|
||||
}
|
||||
|
||||
for (const FShaderLabIntrinsicDesc& D : Descs)
|
||||
{
|
||||
if (!D.Doc.IsEmpty())
|
||||
{
|
||||
Body += FString::Printf(TEXT("\t// %s\n"), *D.Doc);
|
||||
Body += FString::Printf(TEXT("// %s\n"), *D.Doc);
|
||||
}
|
||||
FString Params;
|
||||
for (int32 i = 0; i < D.Params.Num(); ++i)
|
||||
{
|
||||
const FShaderLabIntrinsicParam& P = D.Params[i];
|
||||
if (i > 0) { Params += TEXT(", "); }
|
||||
Params += FString::Printf(TEXT("%s %s"), *P.Type, *P.Name);
|
||||
const FString ParamType = P.Enum ? P.Enum->GetName() : P.Type;
|
||||
Params += FString::Printf(TEXT("%s %s"), *ParamType, *P.Name);
|
||||
if (!P.DefaultLiteral.IsEmpty()) { Params += FString::Printf(TEXT(" = %s"), *P.DefaultLiteral); }
|
||||
}
|
||||
// Stub body returns a zero of the return type; this is authoring-only and never compiled
|
||||
// (the graph builder rewrites UE:: calls into material-expression inputs before compile).
|
||||
Body += FString::Printf(TEXT("\t%s %s(%s) { return (%s)0; }\n"),
|
||||
// (the graph builder rewrites UE_ calls into material-expression inputs before compile).
|
||||
Body += FString::Printf(TEXT("%s UE_%s(%s) { return (%s)0; }\n"),
|
||||
*D.ReturnType, *D.Name.ToString(), *Params, *D.ReturnType);
|
||||
}
|
||||
Body += TEXT("}\n");
|
||||
return Body;
|
||||
}
|
||||
|
||||
@@ -158,41 +197,41 @@ namespace ShaderLabIDEPrepare_Private
|
||||
return true;
|
||||
}
|
||||
|
||||
static void GenerateShaderLabHeader(const FString& PluginShaderDir)
|
||||
// Generates Private/ShaderLabUENode.ush: the UE_ node-intrinsic stubs (+ their enums), from the
|
||||
// registry. Included by the static ShaderLab.ush umbrella; editor-only (the parser strips the shim).
|
||||
static void GenerateUENodeHeader(const FString& PluginShaderDir)
|
||||
{
|
||||
FString H;
|
||||
H += TEXT("// GENERATED by `ShaderLab.IDE.Prepare` — do not edit by hand.\n");
|
||||
H += TEXT("// Authoring umbrella header: include this from every .usl for IDE completion.\n");
|
||||
H += TEXT("// At real shader-compile time this header is NOT included (the parser strips it);\n");
|
||||
H += TEXT("// the UE:: stubs below are for the editor only.\n");
|
||||
H += TEXT("// UE_ node-intrinsic stubs + their enums, for IDE completion only (never compiled).\n");
|
||||
H += TEXT("#pragma once\n\n");
|
||||
H += TEXT("#include \"/Plugin/ShaderLab/ShaderLabDSL.ush\"\n");
|
||||
H += TEXT("#include \"/Plugin/ShaderLab/Private/ShaderLabCommon.ush\"\n\n");
|
||||
H += GenerateIntrinsicNamespace();
|
||||
WriteIfChanged(FPaths::Combine(PluginShaderDir, TEXT("ShaderLab.ush")), H);
|
||||
H += GenerateIntrinsicStubs();
|
||||
WriteIfChanged(FPaths::Combine(PluginShaderDir, TEXT("Private"), TEXT("ShaderLabUENode.ush")), H);
|
||||
}
|
||||
|
||||
static void GenerateShaderToolsConfig(const FString& ProjectDir)
|
||||
static FString SerializeObject(const TSharedRef<FJsonObject>& Obj)
|
||||
{
|
||||
const TMap<FString, FString>& Mappings = AllShaderSourceDirectoryMappings();
|
||||
|
||||
const TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetBoolField(TEXT("root"), true);
|
||||
const TSharedRef<FJsonObject> Virt = MakeShared<FJsonObject>();
|
||||
for (const TPair<FString, FString>& Pair : Mappings)
|
||||
{
|
||||
// Engine mappings come back relative to the engine binaries dir; HLSL Tools needs
|
||||
// absolute (or config-relative) paths, so normalize to full paths.
|
||||
Virt->SetStringField(Pair.Key, FPaths::ConvertRelativePathToFull(Pair.Value));
|
||||
}
|
||||
Root->SetObjectField(TEXT("hlsl.virtualDirectoryMappings"), Virt);
|
||||
|
||||
FString Out;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
|
||||
FJsonSerializer::Serialize(Root, Writer);
|
||||
WriteIfChanged(FPaths::Combine(ProjectDir, TEXT("shadertoolsconfig.json")), Out);
|
||||
FJsonSerializer::Serialize(Obj, Writer);
|
||||
return Out;
|
||||
}
|
||||
|
||||
// Virtual shader roots -> absolute disk paths, for shader-validator.pathRemapping.
|
||||
static TSharedRef<FJsonObject> BuildPathRemapping()
|
||||
{
|
||||
const TSharedRef<FJsonObject> Remap = MakeShared<FJsonObject>();
|
||||
for (const TPair<FString, FString>& Pair : AllShaderSourceDirectoryMappings())
|
||||
{
|
||||
// Engine mappings come back relative to the engine binaries dir; the extension needs
|
||||
// absolute paths, so normalize.
|
||||
Remap->SetStringField(Pair.Key, FPaths::ConvertRelativePathToFull(Pair.Value));
|
||||
}
|
||||
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.
|
||||
static void MergeVSCodeSettings(const FString& ProjectDir)
|
||||
{
|
||||
const FString SettingsPath = FPaths::Combine(ProjectDir, TEXT(".vscode"), TEXT("settings.json"));
|
||||
@@ -205,7 +244,7 @@ namespace ShaderLabIDEPrepare_Private
|
||||
if (!FJsonSerializer::Deserialize(Reader, Settings) || !Settings.IsValid())
|
||||
{
|
||||
UE_LOG(LogShaderLabIDE, Warning,
|
||||
TEXT(" could not parse %s — add files.associations for *.usl/*.usf/*.ush -> hlsl manually."),
|
||||
TEXT(" could not parse %s — add files.associations and shader-validator.pathRemapping manually."),
|
||||
*SettingsPath);
|
||||
return; // Never clobber an unparseable user file.
|
||||
}
|
||||
@@ -215,51 +254,53 @@ namespace ShaderLabIDEPrepare_Private
|
||||
Settings = MakeShared<FJsonObject>();
|
||||
}
|
||||
|
||||
// Union into files.associations, preserving any existing entries. Only rewrite the file when
|
||||
// we actually add a mapping, so a hand-maintained settings.json (comments, formatting) is left
|
||||
// untouched once the associations are present.
|
||||
const TSharedPtr<FJsonObject>* AssocPtr = nullptr;
|
||||
TSharedPtr<FJsonObject> Assoc = Settings->TryGetObjectField(TEXT("files.associations"), AssocPtr)
|
||||
? *AssocPtr : MakeShared<FJsonObject>();
|
||||
bool bChanged = false;
|
||||
|
||||
// files.associations: union in the ShaderLab extensions.
|
||||
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") })
|
||||
{
|
||||
FString Existing2;
|
||||
if (!Assoc->TryGetStringField(Ext, Existing2) || Existing2 != TEXT("hlsl"))
|
||||
FString Value;
|
||||
if (!Assoc->TryGetStringField(Ext, Value) || Value != TEXT("hlsl"))
|
||||
{
|
||||
Assoc->SetStringField(Ext, TEXT("hlsl"));
|
||||
bChanged = true;
|
||||
}
|
||||
}
|
||||
Settings->SetObjectField(TEXT("files.associations"), Assoc);
|
||||
|
||||
// shader-validator.pathRemapping: replace with the current (machine-specific) mapping table.
|
||||
const TSharedRef<FJsonObject> Remap = BuildPathRemapping();
|
||||
const TSharedPtr<FJsonObject>* ExistingRemapPtr = nullptr;
|
||||
const bool bHasRemap = Settings->TryGetObjectField(TEXT("shader-validator.pathRemapping"), ExistingRemapPtr);
|
||||
if (!bHasRemap || SerializeObject(ExistingRemapPtr->ToSharedRef()) != SerializeObject(Remap))
|
||||
{
|
||||
Settings->SetObjectField(TEXT("shader-validator.pathRemapping"), Remap);
|
||||
bChanged = true;
|
||||
}
|
||||
|
||||
// shader-validator.defines: SHADERLAB_IDE gates the editor-only engine-function stubs in
|
||||
// ShaderLabFunctions.ush (so its UE_ forwarders resolve in the IDE without dragging in engine
|
||||
// headers). The real shader compiler never sees this define.
|
||||
const TSharedPtr<FJsonObject>* DefinesPtr = nullptr;
|
||||
const TSharedPtr<FJsonObject> Defines = Settings->TryGetObjectField(TEXT("shader-validator.defines"), DefinesPtr)
|
||||
? *DefinesPtr : MakeShared<FJsonObject>();
|
||||
FString DefineValue;
|
||||
if (!Defines->TryGetStringField(TEXT("SHADERLAB_IDE"), DefineValue) || DefineValue != TEXT("1"))
|
||||
{
|
||||
Defines->SetStringField(TEXT("SHADERLAB_IDE"), TEXT("1"));
|
||||
Settings->SetObjectField(TEXT("shader-validator.defines"), Defines);
|
||||
bChanged = true;
|
||||
}
|
||||
|
||||
if (!bChanged)
|
||||
{
|
||||
UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *SettingsPath);
|
||||
return;
|
||||
}
|
||||
Settings->SetObjectField(TEXT("files.associations"), Assoc);
|
||||
|
||||
FString Out;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
|
||||
FJsonSerializer::Serialize(Settings.ToSharedRef(), Writer);
|
||||
WriteIfChanged(SettingsPath, Out);
|
||||
}
|
||||
|
||||
static void EnsureGitIgnore(const FString& ProjectDir)
|
||||
{
|
||||
const FString Path = FPaths::Combine(ProjectDir, TEXT(".gitignore"));
|
||||
FString GI;
|
||||
FFileHelper::LoadFileToString(GI, *Path); // missing file -> empty, fine
|
||||
if (GI.Contains(TEXT("shadertoolsconfig.json")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!GI.IsEmpty() && !GI.EndsWith(TEXT("\n")))
|
||||
{
|
||||
GI += TEXT("\n");
|
||||
}
|
||||
GI += TEXT("# Machine-specific HLSL Tools config (generated by ShaderLab.IDE.Prepare).\n");
|
||||
GI += TEXT("shadertoolsconfig.json\n");
|
||||
WriteIfChanged(Path, GI);
|
||||
WriteIfChanged(SettingsPath, SerializeObject(Settings.ToSharedRef()));
|
||||
}
|
||||
|
||||
static void Run(const TArray<FString>& /*Args*/)
|
||||
@@ -271,10 +312,8 @@ namespace ShaderLabIDEPrepare_Private
|
||||
const FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
|
||||
|
||||
UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare:"));
|
||||
GenerateShaderLabHeader(PluginShaderDir);
|
||||
GenerateShaderToolsConfig(ProjectDir);
|
||||
GenerateUENodeHeader(PluginShaderDir);
|
||||
MergeVSCodeSettings(ProjectDir);
|
||||
EnsureGitIgnore(ProjectDir);
|
||||
UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare: done."));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user