mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-22 18:24:41 +00:00
Enhance .usl compile error display
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "Misc/CoreDelegates.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "ShaderLabDiscovery.h"
|
||||
#include "ShaderLabMaterialRegistry.h"
|
||||
#include "ShaderLabModel.h"
|
||||
@@ -74,7 +75,28 @@ UMaterial* UShaderLabSubsystem::RebuildFromFile(const FString& FilePath)
|
||||
{
|
||||
UE_LOG(LogShaderLabSubsystem, Error, TEXT("ShaderLab parse error in %s %s"), *FilePath, *E.ToString());
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
// Contract: a broken .usl must not silently keep the last-good material. Register an "error model"
|
||||
// carrying the parse diagnostics — the editor graph builder turns it into a poison material whose
|
||||
// shader fails to compile with these errors mapped back to the source, so they surface through the
|
||||
// standard shader-compile path (MIC editor red text, cook) exactly like an HLSL error. The cooked
|
||||
// runtime has no graph builder, so there it stays a hard skip (a broken .usl is a cook-time bug).
|
||||
FString SrcPath = FPaths::ConvertRelativePathToFull(FilePath);
|
||||
SrcPath.ReplaceInline(TEXT("\\"), TEXT("/"));
|
||||
|
||||
FShaderLabModel ErrorModel;
|
||||
ErrorModel.ShaderName = FPaths::GetBaseFilename(FilePath);
|
||||
ErrorModel.SourceFilePath = FilePath;
|
||||
for (const FShaderLabParseError& E : Errors)
|
||||
{
|
||||
ErrorModel.LoadErrors.Add(FString::Printf(
|
||||
TEXT("%s(%d,%d): error: %s"), *SrcPath, E.Line, E.Column, *E.Message));
|
||||
}
|
||||
return FShaderLabMaterialRegistry::Get().RegisterFromModel(ErrorModel);
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
return FShaderLabMaterialRegistry::Get().RegisterFromModel(Model);
|
||||
|
||||
@@ -148,6 +148,15 @@ struct USHADERLAB_API FShaderLabModel
|
||||
/** Source file this model was parsed from (for diagnostics / hashing context). */
|
||||
FString SourceFilePath;
|
||||
|
||||
/**
|
||||
* Diagnostics captured when a valid model could NOT be built (e.g. parse errors). When non-empty this
|
||||
* is an "error model": only ShaderName/SourceFilePath are meaningful, and the editor graph builder
|
||||
* emits a poison material whose shader deliberately fails to compile with these messages (mapped back
|
||||
* to the .usl). That routes parse errors through the standard shader-compile path — MIC editor red
|
||||
* text, GetCompileErrors, cook — exactly like an HLSL error, instead of being silently swallowed.
|
||||
*/
|
||||
TArray<FString> LoadErrors;
|
||||
|
||||
FShaderLabSettings Settings;
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,12 @@ public:
|
||||
/** Scan every search root and (re)register all discovered shaders. */
|
||||
void DiscoverAndRegisterAll();
|
||||
|
||||
/** Parse a single `.usl` file and (re)register its material. Returns null on parse error. */
|
||||
/**
|
||||
* Parse a single `.usl` file and (re)register its material. In the editor a parse error still
|
||||
* registers a poison base (carrying the diagnostics) so the error surfaces through the shader
|
||||
* compile path; only a file-read failure (or the cooked runtime, which has no graph builder)
|
||||
* returns null.
|
||||
*/
|
||||
UMaterial* RebuildFromFile(const FString& FilePath);
|
||||
|
||||
private:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
|
||||
#include "DirectoryWatcherModule.h"
|
||||
#include "Editor.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "IDirectoryWatcher.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "MaterialEditingLibrary.h"
|
||||
#include "MaterialShared.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Misc/Paths.h"
|
||||
@@ -23,14 +25,36 @@ namespace
|
||||
/** Editor-side reaction to a base material needing build: rebuild graph + trigger compile. */
|
||||
void BuildAndCompile(UMaterial& Material, const FShaderLabModel& Model)
|
||||
{
|
||||
TArray<FString> Errors;
|
||||
if (!FShaderLabGraphBuilder::BuildInto(Material, Model, Errors))
|
||||
// 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)
|
||||
{
|
||||
for (const FString& E : Errors)
|
||||
// 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(LogShaderLabEditor, Error, TEXT("ShaderLab build '%s': %s"), *Model.ShaderName, *E);
|
||||
UE_LOG(LogShaderLabEditor, 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(LogShaderLabEditor, 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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Recompile the base AND propagate to everything that depends on it. A bare PostEditChange
|
||||
@@ -38,10 +62,23 @@ namespace
|
||||
// (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();
|
||||
{
|
||||
FMaterialUpdateContext UpdateContext;
|
||||
UpdateContext.AddMaterial(&Material);
|
||||
Material.PreEditChange(nullptr);
|
||||
Material.PostEditChange();
|
||||
}
|
||||
|
||||
// Refresh any open Material Instance editors built on this base so newly added/removed parameters
|
||||
// (scalars, textures, static-bool switches) appear without reopening the editor. RegenerateArrays
|
||||
// pulls the parameter set from the freshly rebuilt parent; mirrors what the stock material editor
|
||||
// does after RecompileMaterial. Must run after PostEditChange so the parent's parameters are current.
|
||||
// Guarded on GEditor: this handler also runs during cook/headless registration, where there is no
|
||||
// interactive editor (and RebuildMaterialInstanceEditors dereferences GEditor).
|
||||
if (GEditor)
|
||||
{
|
||||
UMaterialEditingLibrary::RebuildMaterialInstanceEditors(&Material);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,15 +113,6 @@ public:
|
||||
|
||||
StartWatchingSources();
|
||||
|
||||
// Note: ShaderLab base materials are deliberately NOT surfaced to the Content Browser — they are
|
||||
// hidden templates. Users create UShaderLabMaterialInstanceConstant instances via the factory
|
||||
// (Content Browser ▸ Material ▸ ShaderLab Material Instance), which picks a base directly from
|
||||
// the registry. The instances are the only assets users interact with.
|
||||
//
|
||||
// There is NO cook-time hook: the .usl sources are staged into packaged builds (a wildcard
|
||||
// RuntimeDependency in ShaderLab.Build.cs) and the cooked runtime re-parses them directly, so
|
||||
// nothing special needs to be written at cook.
|
||||
|
||||
UE_LOG(LogShaderLabEditor, Log, TEXT("ShaderLabEditor module started."));
|
||||
}
|
||||
|
||||
|
||||
@@ -1194,3 +1194,51 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
Material.UpdateCachedExpressionData();
|
||||
return true;
|
||||
}
|
||||
|
||||
void FShaderLabGraphBuilder::BuildPoisonInto(UMaterial& Material, const TArray<FString>& Diagnostics, const FString& SrcPath)
|
||||
{
|
||||
using namespace ShaderLabGraph;
|
||||
|
||||
// A minimal, structurally-valid Substrate material: one Custom node -> Slab.DiffuseAlbedo -> FrontMaterial,
|
||||
// so the Custom code is reached by translation. The code `#error`s with the diagnostics; that aborts the
|
||||
// shader preprocessor, so the material fails to compile with our messages — surfacing everywhere real
|
||||
// shader errors do (MIC editor red text, FMaterialResource::GetCompileErrors, cook). DiffuseAlbedo (a core
|
||||
// BSDF pin) is used deliberately: EmissiveColor gets dead-stripped from the compiled permutation, which
|
||||
// would drop the Custom function (and its #error) before the preprocessor ever sees it.
|
||||
Material.AssignExpressionCollection(FMaterialExpressionCollection());
|
||||
Material.MaterialDomain = MD_Surface;
|
||||
Material.BlendMode = BLEND_Opaque;
|
||||
Material.TwoSided = 0;
|
||||
Material.bUseMaterialAttributes = false;
|
||||
|
||||
UMaterialEditorOnlyData* EditorOnly = Material.GetEditorOnlyData();
|
||||
check(EditorOnly); // A UMaterial always has editor-only data in the editor (contract).
|
||||
|
||||
// `#error` halts at the first hit, so merge every diagnostic into one directive (newlines stripped to
|
||||
// keep it a single preprocessor line). The `#line` makes the compiler error click through to the .usl;
|
||||
// each merged message still carries its own precise `(line,col)` as text.
|
||||
FString Combined;
|
||||
for (int32 Index = 0; Index < Diagnostics.Num(); ++Index)
|
||||
{
|
||||
if (Index > 0) { Combined += TEXT(" ; "); }
|
||||
Combined += Diagnostics[Index];
|
||||
}
|
||||
Combined.ReplaceInline(TEXT("\r"), TEXT(""));
|
||||
Combined.ReplaceInline(TEXT("\n"), TEXT(" "));
|
||||
if (Combined.IsEmpty()) { Combined = TEXT("ShaderLab: shader failed to build"); }
|
||||
|
||||
int32 IoY = 0;
|
||||
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
|
||||
Custom->Description = TEXT("ShaderLab Error");
|
||||
Custom->OutputType = CMOT_Float3;
|
||||
Custom->Code = FString::Printf(
|
||||
TEXT("#line 1 \"%s\"\n#error ShaderLab: %s\n#line 1 \"ShaderLabGenerated.ush\"\nreturn float3(1,0,1);\n"),
|
||||
*SrcPath, *Combined);
|
||||
Custom->RebuildOutputs();
|
||||
|
||||
UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr<UMaterialExpressionSubstrateSlabBSDF>(Material, IoY, 0);
|
||||
Slab->DiffuseAlbedo.Connect(0, Custom);
|
||||
EditorOnly->FrontMaterial.Connect(0, Slab);
|
||||
|
||||
Material.UpdateCachedExpressionData();
|
||||
}
|
||||
|
||||
@@ -25,4 +25,14 @@ public:
|
||||
* compilation — callers decide when to compile. Returns false with diagnostics on failure.
|
||||
*/
|
||||
static bool BuildInto(UMaterial& Material, const FShaderLabModel& Model, TArray<FString>& OutErrors);
|
||||
|
||||
/**
|
||||
* Replace `Material`'s graph with a minimal Substrate material whose single Custom node emits a
|
||||
* `#line`-mapped `#error` for `Diagnostics`, so it deliberately fails to compile with those messages
|
||||
* mapped back to `SrcPath` (an absolute, forward-slashed .usl path). This routes .usl parse/build
|
||||
* errors through the standard shader-compile path — MIC editor red text, GetCompileErrors, cook —
|
||||
* instead of dropping them. The graph itself is always valid; the failure is intentional and happens
|
||||
* at shader compile. Does NOT trigger compilation (the caller decides when).
|
||||
*/
|
||||
static void BuildPoisonInto(UMaterial& Material, const TArray<FString>& Diagnostics, const FString& SrcPath);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user