GraphSettings is a model describing non-node metadata for a shader document: domain, shading model, blend/cull settings, declared render passes (Modes), backend targets, HLSL dialect, debug/strict flags and misc data. It provides helpers to query and mutate targets/modes, normalize the lists, clone/copy, and compute derived booleans/stage mask.
using Editor.Prism.Compiler.Backends;
using Editor.Prism.Core;
namespace Editor.Prism.Model;
/// <summary>
/// Everything about a document that is not nodes, edges or parameters: what the graph is for, how it
/// resolves to pixels, which render passes it participates in and which artifacts it emits on save.
/// <para>
/// Mutable by design — the Inspector edits it in place through <c>GraphMutations.UpdateSettings</c>,
/// which snapshots the document around the edit so the change is undoable.
/// </para>
/// </summary>
public sealed class GraphSettings
{
/// <summary>Build settings with the shipped defaults: a lit, opaque, back-face-culled surface shader.</summary>
public GraphSettings()
{
Modes = new List<string>( DefaultModes );
Targets = new List<string> { PrismConstants.BackendHlsl };
}
/// <summary>What the graph is for.</summary>
public ShaderDomain Domain { get; set; } = ShaderDomain.Surface;
/// <summary>How a surface graph resolves to pixels.</summary>
public ShadingModel ShadingModel { get; set; } = ShadingModel.Lit;
/// <summary>Output blending.</summary>
public SurfaceBlendMode BlendMode { get; set; } = SurfaceBlendMode.Opaque;
/// <summary>Triangle culling.</summary>
public CullMode CullMode { get; set; } = CullMode.Back;
/// <summary>Declared render passes, e.g. <c>Forward</c>, <c>Depth</c>, <c>ToolsShadingComplexity</c>.</summary>
public List<string> Modes { get; set; }
/// <summary>Backend ids emitted on save. Always contains the HLSL backend for a shader graph.</summary>
public List<string> Targets { get; set; }
/// <summary>Which HLSL flavour the generated <c>.shader</c> uses.</summary>
public HlslDialect HlslDialect { get; set; } = HlslDialect.SboxSlang;
/// <summary>Whether the second UV channel is declared on the vertex input.</summary>
public bool Uv2 { get; set; }
/// <summary>Whether back faces are rendered regardless of <see cref="CullMode"/>.</summary>
public bool RenderBackfaces { get; set; }
/// <summary>Whether generated code gets descriptive temp names and per-node comments.</summary>
public bool DebugSymbols { get; set; }
/// <summary>
/// Treat every lossy or padding conversion as an error rather than a warning. Teams that want the
/// type system to be unforgiving turn this on; it never changes what is generated, only severity.
/// </summary>
public bool StrictTypes { get; set; }
/// <summary>Forward-compatibility bag preserved verbatim through a round-trip.</summary>
public JsonObject X { get; set; }
/// <summary>True when the blend mode needs <c>S_TRANSLUCENT</c> defined before <c>common/pixel.hlsl</c>.</summary>
public bool IsTranslucent =>
BlendMode is SurfaceBlendMode.Translucent or SurfaceBlendMode.Additive or SurfaceBlendMode.Multiply;
/// <summary>True when the blend mode needs <c>S_ALPHA_TEST</c> defined before <c>common/pixel.hlsl</c>.</summary>
public bool IsAlphaTest => BlendMode == SurfaceBlendMode.Masked;
/// <summary>True when the graph produces a full lighting evaluation rather than raw colour.</summary>
public bool IsLit => Domain == ShaderDomain.Surface && ShadingModel == ShadingModel.Lit;
/// <summary>The stages the generated shader declares.</summary>
public StageMask Stages => Domain switch
{
ShaderDomain.Compute => StageMask.Compute,
ShaderDomain.PostProcess => StageMask.VertexPixel,
_ => StageMask.VertexPixel
};
/// <summary>True when the given backend id is in <see cref="Targets"/>.</summary>
public bool WantsTarget( string backendId )
{
if ( string.IsNullOrEmpty( backendId ) ) return false;
if ( Targets is null ) return false;
foreach ( var target in Targets )
{
if ( string.Equals( target, backendId, StringComparison.OrdinalIgnoreCase ) ) return true;
}
return false;
}
/// <summary>Add or remove a backend id from <see cref="Targets"/>, keeping the list free of duplicates.</summary>
public void SetTarget( string backendId, bool wanted )
{
if ( string.IsNullOrEmpty( backendId ) ) return;
Targets ??= new List<string>();
Targets.RemoveAll( x => string.Equals( x, backendId, StringComparison.OrdinalIgnoreCase ) );
if ( wanted ) Targets.Add( backendId );
}
/// <summary>True when the given render pass is declared.</summary>
public bool HasMode( string mode ) =>
Modes is not null && Modes.Any( x => string.Equals( x, mode, StringComparison.OrdinalIgnoreCase ) );
/// <summary>Add or remove a render pass, keeping the list free of duplicates.</summary>
public void SetMode( string mode, bool wanted )
{
if ( string.IsNullOrEmpty( mode ) ) return;
Modes ??= new List<string>();
Modes.RemoveAll( x => string.Equals( x, mode, StringComparison.OrdinalIgnoreCase ) );
if ( wanted ) Modes.Add( mode );
}
/// <summary>
/// Drop anything that would produce an invalid shader: unknown render passes, unknown backend ids
/// and an empty target list. Reports what it changed and never throws.
/// </summary>
public void Normalize( DiagnosticSink sink = null )
{
Modes ??= new List<string>();
Targets ??= new List<string>();
for ( int i = Modes.Count - 1; i >= 0; i-- )
{
var mode = Modes[i];
if ( !string.IsNullOrWhiteSpace( mode ) && KnownModes.Contains( mode, StringComparer.OrdinalIgnoreCase ) )
{
continue;
}
sink?.Warn( DiagnosticCode.SectionReadFailed, $"Unknown render mode '{mode}' was dropped" );
Modes.RemoveAt( i );
}
for ( int i = Targets.Count - 1; i >= 0; i-- )
{
var target = Targets[i];
if ( !string.IsNullOrWhiteSpace( target ) &&
s_targetAliases.TryGetValue( target, out var canonical ) )
{
Targets[i] = canonical;
continue;
}
sink?.Warn( DiagnosticCode.SectionReadFailed, $"Unknown target '{target}' was dropped" );
Targets.RemoveAt( i );
}
Modes = Dedupe( Modes );
Targets = Dedupe( Targets );
if ( Modes.Count == 0 ) Modes.AddRange( DefaultModes );
if ( Targets.Count == 0 ) Targets.Add( PrismConstants.BackendHlsl );
}
/// <summary>Deep copy.</summary>
public GraphSettings Clone() => new()
{
Domain = Domain,
ShadingModel = ShadingModel,
BlendMode = BlendMode,
CullMode = CullMode,
Modes = Modes is null ? new List<string>( DefaultModes ) : new List<string>( Modes ),
Targets = Targets is null ? new List<string> { PrismConstants.BackendHlsl } : new List<string>( Targets ),
HlslDialect = HlslDialect,
Uv2 = Uv2,
RenderBackfaces = RenderBackfaces,
DebugSymbols = DebugSymbols,
StrictTypes = StrictTypes,
X = X?.DeepClone() as JsonObject
};
/// <summary>Copy every field from another instance without replacing the object identity.</summary>
public void CopyFrom( GraphSettings other )
{
if ( other is null ) return;
Domain = other.Domain;
ShadingModel = other.ShadingModel;
BlendMode = other.BlendMode;
CullMode = other.CullMode;
Modes = other.Modes is null ? new List<string>( DefaultModes ) : new List<string>( other.Modes );
Targets = other.Targets is null
? new List<string> { PrismConstants.BackendHlsl }
: new List<string>( other.Targets );
HlslDialect = other.HlslDialect;
Uv2 = other.Uv2;
RenderBackfaces = other.RenderBackfaces;
DebugSymbols = other.DebugSymbols;
StrictTypes = other.StrictTypes;
X = other.X?.DeepClone() as JsonObject;
}
/// <inheritdoc/>
public override string ToString() => $"{Domain}/{ShadingModel}/{BlendMode}";
/// <summary>The render passes a generated shader may declare. Anything else fails the block parser.</summary>
public static readonly IReadOnlyList<string> KnownModes = new[]
{
"Forward", "VrForward", "Default", "Depth", "ToolsShadingComplexity", "ToolsUtil"
};
/// <summary>The backend ids a document may target.</summary>
public static readonly IReadOnlyList<string> KnownTargets = new[]
{
PrismConstants.BackendHlsl, PrismConstants.BackendSlang
};
/// <summary>
/// Spellings accepted for a target on read, mapped to the canonical backend id. Documents written
/// against the design notes use friendly names; the backends are keyed by id.
/// </summary>
static readonly Dictionary<string, string> s_targetAliases = new( StringComparer.OrdinalIgnoreCase )
{
[PrismConstants.BackendHlsl] = PrismConstants.BackendHlsl,
["SboxShader"] = PrismConstants.BackendHlsl,
["Shader"] = PrismConstants.BackendHlsl,
["Hlsl"] = PrismConstants.BackendHlsl,
[PrismConstants.BackendSlang] = PrismConstants.BackendSlang,
["Slang"] = PrismConstants.BackendSlang
};
/// <summary>The passes a new surface graph declares.</summary>
public static readonly IReadOnlyList<string> DefaultModes = new[]
{
"Forward", "Depth", "ToolsShadingComplexity"
};
/// <summary>The minimal pass set a preview compile declares — combos cost time we do not have.</summary>
public static readonly IReadOnlyList<string> PreviewModes = new[] { "Forward" };
static List<string> Dedupe( List<string> values )
{
var seen = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
var result = new List<string>( values.Count );
foreach ( var value in values )
{
if ( !seen.Add( value ) ) continue;
result.Add( value );
}
return result;
}
}