Editor Prism node implementations for graph parameters, constants and keyword-based branching. Defines PrismConstantKind enum, PrismParameters helper functions (parameter/keyword lookup, symbol generation, component helpers), and node classes: ParameterReferenceNode, ConstantNode, KeywordBranchNode, KeywordValueNode, StaticSwitchNode, ShaderConstantNode. They validate, define ports, and emit IR/global declarations for use by the compiler/backends.
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Backends;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;
namespace Editor.Prism.Nodes;
// ---------------------------------------------------------------------------------------------------
// Parameters, constants and combos.
//
// A parameter node stores a ParamId, never a name: renaming a blackboard entry must not break a graph,
// and the clipboard has to be able to remap the reference exactly when a fragment is pasted into a
// document that already has that id. The uniform symbol is derived through
// GraphCompiler.ParameterSymbol so the model, the compiler and both backends agree on one spelling.
// ---------------------------------------------------------------------------------------------------
/// <summary>The shapes a constant node can take.</summary>
public enum PrismConstantKind
{
/// <summary>A single float.</summary>
Float,
/// <summary>Two floats.</summary>
Float2,
/// <summary>Three floats.</summary>
Float3,
/// <summary>Four floats.</summary>
Float4,
/// <summary>A colour, edited through a colour picker and split into RGBA.</summary>
Color,
/// <summary>A boolean.</summary>
Bool,
/// <summary>A signed integer.</summary>
Int,
/// <summary>A 4x4 matrix, authored as four rows.</summary>
Matrix
}
/// <summary>Naming and typing rules shared by every node that references a blackboard entry.</summary>
public static class PrismParameters
{
/// <summary>The declaration the compiler emits for a parameter. Matches what the compiler builds itself.</summary>
public static GlobalDecl Declare( IGraphParameter parameter )
{
if ( parameter is null ) return null;
var name = GraphCompiler.ParameterSymbol( parameter );
if ( string.IsNullOrEmpty( name ) ) return null;
return new GlobalDecl( name, parameter.Type, KindFor( parameter.Type ) )
{
Parameter = parameter.Id,
AttributeName = parameter.AttributeName,
Default = parameter.Type.IsNumeric ? parameter.DefaultValue : null,
DefaultAsset = parameter.DefaultAsset,
Srgb = parameter.Srgb,
Ui = parameter.Ui
};
}
/// <summary>Which kind of module-level declaration a type becomes.</summary>
public static GlobalKind KindFor( ShaderType type )
{
if ( type.IsSampler ) return GlobalKind.Sampler;
if ( type.IsTexture ) return type.IsWritable ? GlobalKind.RwTexture : GlobalKind.Texture;
if ( type.IsBuffer ) return type.IsWritable ? GlobalKind.RwBuffer : GlobalKind.Buffer;
return GlobalKind.Uniform;
}
/// <summary>Find a blackboard parameter by id. Null when the graph does not have it.</summary>
public static IGraphParameter Find( IPrismGraph graph, ParamId id )
{
if ( graph is not ICompilableGraph compilable || compilable.Parameters is null ) return null;
if ( !id.IsValid ) return null;
foreach ( var parameter in compilable.Parameters )
{
if ( parameter is not null && parameter.Id == id ) return parameter;
}
return null;
}
/// <summary>Find a keyword by id. Null when the graph does not have it.</summary>
public static IGraphKeyword FindKeyword( IPrismGraph graph, ParamId id )
{
if ( graph is not ICompilableGraph compilable || compilable.Keywords is null ) return null;
if ( !id.IsValid ) return null;
foreach ( var keyword in compilable.Keywords )
{
if ( keyword is not null && keyword.Id == id ) return keyword;
}
return null;
}
/// <summary>
/// The symbol a combo is readable under from inside a program. A feature is only visible through
/// the static combo it is bound to, so <c>F_PUDDLES</c> is read as <c>S_PUDDLES</c>.
/// </summary>
public static string ComboSymbol( IGraphKeyword keyword )
{
if ( keyword is null || string.IsNullOrWhiteSpace( keyword.Name ) ) return null;
return keyword.Kind == ComboKind.Feature
? SboxShaderWriter.StaticNameFor( keyword.Name )
: keyword.Name;
}
/// <summary>
/// The symbol a combo is readable under from a standalone Slang module. The Slang backend lowers
/// every graph keyword to a link-time specialization constant named after the keyword itself, so a
/// feature is read as <c>F_PUDDLES</c> there and as <c>S_PUDDLES</c> in a <c>.shader</c>.
/// </summary>
public static string SlangComboSymbol( IGraphKeyword keyword ) =>
string.IsNullOrWhiteSpace( keyword?.Name ) ? null : keyword.Name;
/// <summary>
/// A helper that reads a combo as a boolean.
/// <para>
/// The HLSL body is guarded by <c>#if defined</c>, because a <c>.shader</c> can legitimately be
/// compiled without the combo — a probe build, or a pass it was never wired into. The Slang body is
/// not: a module always declares every keyword as a specialization constant, and a constant is
/// invisible to the preprocessor, so the guard would make the branch permanently false.
/// </para>
/// </summary>
public static HelperFunction ComboTest( string symbol, string slangSymbol = null )
{
var name = "Prism_Combo_" + symbol;
return new HelperFunction( name, ShaderType.Bool, Array.Empty<HelperParam>() )
{
Hlsl = TestBody( name, symbol, true ),
Slang = TestBody( name, string.IsNullOrEmpty( slangSymbol ) ? symbol : slangSymbol, false )
};
}
/// <summary>
/// A helper that reads a combo as an integer, for a multi-valued combo used as a mode selector.
/// </summary>
public static HelperFunction ComboValue( string symbol, string slangSymbol = null )
{
var name = "Prism_ComboValue_" + symbol;
return new HelperFunction( name, ShaderType.Int, Array.Empty<HelperParam>() )
{
Hlsl = ValueBody( name, symbol, true ),
Slang = ValueBody( name, string.IsNullOrEmpty( slangSymbol ) ? symbol : slangSymbol, false )
};
}
static string TestBody( string name, string symbol, bool guarded ) =>
Body( "bool", name, $"return ( {symbol} ) != 0;", "return false;", symbol, guarded );
static string ValueBody( string name, string symbol, bool guarded ) =>
Body( "int", name, $"return (int)( {symbol} );", "return 0;", symbol, guarded );
static string Body( string type, string name, string taken, string fallback, string symbol, bool guarded )
{
if ( !guarded ) return $"{type} {name}()\r\n{{\r\n\t{taken}\r\n}}";
return
$"{type} {name}()\r\n" +
"{\r\n" +
$"#if defined( {symbol} )\r\n" +
$"\t{taken}\r\n" +
"#else\r\n" +
$"\t{fallback}\r\n" +
"#endif\r\n" +
"}";
}
/// <summary>The component labels a type's split outputs use.</summary>
public static IReadOnlyList<string> ComponentNames( ShaderType type, bool asColor )
{
var count = Math.Clamp( type.Components, 0, 4 );
if ( count <= 1 || type.IsMatrix ) return Array.Empty<string>();
var names = asColor ? new[] { "R", "G", "B", "A" } : new[] { "X", "Y", "Z", "W" };
return names.Take( count ).ToArray();
}
/// <summary>The swizzle mask that selects one component by index.</summary>
public static string ComponentMask( int index ) => index switch
{
1 => "y",
2 => "z",
3 => "w",
_ => "x"
};
}
/// <summary>
/// Reads a blackboard parameter.
/// <para>
/// The reference is a <see cref="ParamId"/>, so renaming the parameter, reordering the blackboard or
/// pasting the node into another document never silently binds it to something else. The uniform is
/// declared once per module however many nodes reference it.
/// </para>
/// </summary>
[NodeInfo( Id = ParameterReferenceNode.TypeId, Title = "Parameter", Category = "Parameter",
Icon = "tune", Keywords = new[] { "parameter", "uniform", "property", "blackboard", "material" },
Description = "Reads a parameter from the graph's blackboard. Drag a parameter onto the canvas to " +
"create one of these already bound." )]
[NodeVersion( 1 )]
public sealed class ParameterReferenceNode : PrismNode
{
/// <summary>The stable type id. Referenced by the legacy importer when it promotes a constant.</summary>
public const string TypeId = "prism.parameter.ref";
ParamId _parameter;
string _parameterType = ShaderType.Float.ToString();
/// <summary>
/// Which blackboard parameter this node reads.
/// <para>
/// Typed as <see cref="ParamId"/> per addendum B4, so the clipboard's paste-time renumbering can
/// find and rewrite it. <c>PrismJson</c> writes it as the bare id string, which is what the document
/// schema shows and what the legacy importer emits.
/// </para>
/// </summary>
public ParamId Parameter
{
get => _parameter;
set
{
if ( _parameter == value ) return;
_parameter = value;
SyncType();
RebuildPorts();
}
}
/// <summary>
/// The parameter's type, mirrored onto the node.
/// <para>
/// Kept so the node still has the right ports — and its wires still draw — when the parameter it
/// points at has been deleted, or when the node is being inspected outside a document.
/// </para>
/// </summary>
public string ParameterType
{
get => _parameterType;
set
{
var text = string.IsNullOrWhiteSpace( value ) ? ShaderType.Float.ToString() : value.Trim();
if ( string.Equals( _parameterType, text, StringComparison.Ordinal ) ) return;
_parameterType = text;
RebuildPorts();
}
}
/// <summary>True when the referenced parameter is a colour, so its channels are labelled RGBA.</summary>
public bool IsColor { get; set; }
/// <summary>The type this node resolves to right now.</summary>
public ShaderType ResolvedParameterType =>
ShaderType.TryParse( _parameterType, out var type ) && !type.IsVoid ? type : ShaderType.Float;
/// <inheritdoc/>
protected override void OnDefinePorts( PortBuilder b )
{
if ( b is null ) return;
SyncType();
var type = ResolvedParameterType;
b.Output( "Out", type.ToString(), string.Empty );
var components = PrismParameters.ComponentNames( type, IsColor );
for ( int i = 0; i < components.Count; i++ )
{
b.Output( components[i], type.ScalarType.ToString(), components[i], order: i + 1 );
}
}
/// <summary>Pull the parameter's type and colour-ness off the blackboard, when there is one.</summary>
void SyncType()
{
var parameter = PrismParameters.Find( Graph, _parameter );
if ( parameter is null ) return;
_parameterType = parameter.Type.ToString();
IsColor = parameter.Ui is { Control: UiControl.Color };
}
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
if ( ctx is null ) return;
if ( !_parameter.IsValid )
{
ctx.Error( "This node is not bound to a parameter", null, DiagnosticCode.UnresolvedParameter );
return;
}
if ( PrismParameters.Find( ctx.Graph, _parameter ) is not null ) return;
ctx.Error( $"The blackboard has no parameter '{_parameter}' any more",
null, DiagnosticCode.UnresolvedParameter );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var parameter = PrismParameters.Find( ctx.Graph, _parameter );
if ( parameter is null )
{
ctx.Error( _parameter.IsValid
? $"The blackboard has no parameter '{_parameter}'"
: "This node is not bound to a parameter" );
ctx.Out( "Out", IrValue.Invalid );
return;
}
var declaration = PrismParameters.Declare( parameter );
if ( declaration is null )
{
ctx.Error( $"Parameter '{parameter.Name}' cannot be declared" );
ctx.Out( "Out", IrValue.Invalid );
return;
}
var value = ctx.Global( declaration );
ctx.Out( "Out", value );
if ( !value.IsValid ) return;
var components = PrismParameters.ComponentNames( parameter.Type, IsColor );
for ( int i = 0; i < components.Count; i++ )
{
ctx.Out( components[i], ctx.Swizzle( value, PrismParameters.ComponentMask( i ) ) );
}
}
}
/// <summary>
/// A typed literal.
/// <para>
/// In preview mode a constant becomes a live uniform rather than a baked number, so dragging its
/// value updates the frame without recompiling anything; in a final build it folds away entirely.
/// </para>
/// </summary>
[NodeInfo( Id = ConstantNode.TypeId, Title = "Constant", Category = "Parameter",
Icon = "looks_one", Keywords = new[] { "constant", "value", "float", "vector", "colour", "color", "literal" },
Description = "A literal of any shape: scalar, vector, colour, boolean, integer or matrix." )]
[NodeVersion( 1 )]
public sealed class ConstantNode : PrismNode
{
/// <summary>The stable type id. Every legacy Float/Float2/Float3/Float4 node imports to this.</summary>
public const string TypeId = "prism.parameter.constant";
PrismConstantKind _kind = PrismConstantKind.Float;
/// <summary>What shape the literal takes.</summary>
public PrismConstantKind Kind
{
get => _kind;
set
{
if ( _kind == value ) return;
_kind = value;
RebuildPorts();
}
}
/// <summary>The value, when the kind is <see cref="PrismConstantKind.Float"/>.</summary>
public float ScalarValue { get; set; }
/// <summary>The value, when the kind is <see cref="PrismConstantKind.Float2"/>.</summary>
public Vector2 Vector2Value { get; set; }
/// <summary>The value, when the kind is <see cref="PrismConstantKind.Float3"/>.</summary>
public Vector3 Vector3Value { get; set; }
/// <summary>The value, when the kind is <see cref="PrismConstantKind.Float4"/>.</summary>
public Vector4 Vector4Value { get; set; }
/// <summary>The value, when the kind is <see cref="PrismConstantKind.Color"/>.</summary>
public Color ColorValue { get; set; } = Color.White;
/// <summary>The value, when the kind is <see cref="PrismConstantKind.Bool"/>.</summary>
public bool BoolValue { get; set; }
/// <summary>The value, when the kind is <see cref="PrismConstantKind.Int"/>.</summary>
public int IntValue { get; set; }
/// <summary>First row, when the kind is <see cref="PrismConstantKind.Matrix"/>.</summary>
public Vector4 Row0 { get; set; } = new( 1f, 0f, 0f, 0f );
/// <summary>Second row, when the kind is <see cref="PrismConstantKind.Matrix"/>.</summary>
public Vector4 Row1 { get; set; } = new( 0f, 1f, 0f, 0f );
/// <summary>Third row, when the kind is <see cref="PrismConstantKind.Matrix"/>.</summary>
public Vector4 Row2 { get; set; } = new( 0f, 0f, 1f, 0f );
/// <summary>Fourth row, when the kind is <see cref="PrismConstantKind.Matrix"/>.</summary>
public Vector4 Row3 { get; set; } = new( 0f, 0f, 0f, 1f );
/// <summary>The shader type this constant produces.</summary>
public ShaderType ValueType => _kind switch
{
PrismConstantKind.Float2 => ShaderType.Float2,
PrismConstantKind.Float3 => ShaderType.Float3,
PrismConstantKind.Float4 or PrismConstantKind.Color => ShaderType.Float4,
PrismConstantKind.Bool => ShaderType.Bool,
PrismConstantKind.Int => ShaderType.Int,
PrismConstantKind.Matrix => ShaderType.Float4x4,
_ => ShaderType.Float
};
/// <inheritdoc/>
protected override void OnDefinePorts( PortBuilder b )
{
if ( b is null ) return;
var type = ValueType;
b.Output( "Out", type.ToString(), string.Empty );
var components = PrismParameters.ComponentNames( type, _kind == PrismConstantKind.Color );
for ( int i = 0; i < components.Count; i++ )
{
b.Output( components[i], type.ScalarType.ToString(), components[i], order: i + 1 );
}
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = _kind switch
{
PrismConstantKind.Float2 => ctx.Const( Vector2Value ),
PrismConstantKind.Float3 => ctx.Const( Vector3Value ),
PrismConstantKind.Float4 => ctx.Const( Vector4Value ),
PrismConstantKind.Color => ctx.Const( ColorValue ),
PrismConstantKind.Bool => ctx.Const( BoolValue ),
PrismConstantKind.Int => ctx.Const( IntValue ),
PrismConstantKind.Matrix => ctx.Construct( ShaderType.Float4x4,
ctx.Const( Row0 ), ctx.Const( Row1 ), ctx.Const( Row2 ), ctx.Const( Row3 ) ),
_ => ctx.Const( ScalarValue )
};
ctx.Out( "Out", value );
if ( !value.IsValid ) return;
var components = PrismParameters.ComponentNames( ValueType, _kind == PrismConstantKind.Color );
for ( int i = 0; i < components.Count; i++ )
{
ctx.Out( components[i], ctx.Swizzle( value, PrismParameters.ComponentMask( i ) ) );
}
}
}
/// <summary>
/// Chooses between two branches on the state of a graph keyword.
/// <para>
/// A feature or static combo is resolved when the shader is compiled, so the branch that loses costs
/// nothing at runtime — the driver folds the whole <c>select</c> away because the condition is a
/// compile-time constant. A dynamic combo resolves per draw call instead.
/// </para>
/// </summary>
[NodeInfo( Id = KeywordBranchNode.TypeId, Title = "Keyword Branch", Category = "Parameter",
Icon = "call_split", Keywords = new[] { "keyword", "combo", "feature", "static switch", "branch", "#if" },
Description = "Picks one of two values based on a graph keyword. Features and static combos are " +
"resolved at compile time." )]
[NodeVersion( 1 )]
public sealed class KeywordBranchNode : PrismNode
{
/// <summary>The stable type id.</summary>
public const string TypeId = "prism.parameter.keyword";
/// <summary>
/// Which keyword decides the branch. Typed as <see cref="ParamId"/> per addendum B4 so the
/// clipboard can renumber it; written to disk as the bare id string.
/// </summary>
public ParamId Keyword { get; set; }
/// <summary>The value used while the keyword is on.</summary>
[In( "T", Name = "On" )] public PortRef On { get; set; }
/// <summary>The value used while the keyword is off.</summary>
[In( "T", Name = "Off" )] public PortRef Off { get; set; }
/// <summary>The chosen value.</summary>
[Out( "T", Name = "" )] public PortRef Out { get; set; }
/// <summary>The value used when <see cref="On"/> is unconnected.</summary>
[InlineValue( nameof( On ) )] public float DefaultOn { get; set; } = 1f;
/// <summary>The value used when <see cref="Off"/> is unconnected.</summary>
[InlineValue( nameof( Off ) )] public float DefaultOff { get; set; }
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
if ( ctx is null ) return;
if ( !Keyword.IsValid )
{
ctx.Error( "This node is not bound to a keyword", null, DiagnosticCode.UnresolvedParameter );
return;
}
if ( PrismParameters.FindKeyword( ctx.Graph, Keyword ) is not null ) return;
ctx.Error( $"The graph has no keyword '{Keyword}' any more", null, DiagnosticCode.UnresolvedParameter );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var (on, off) = ctx.InPair( nameof( On ), nameof( Off ) );
if ( !on.IsValid || !off.IsValid )
{
ctx.Out( nameof( Out ), on.IsValid ? on : off );
return;
}
var keyword = PrismParameters.FindKeyword( ctx.Graph, Keyword );
var symbol = PrismParameters.ComboSymbol( keyword );
if ( string.IsNullOrEmpty( symbol ) )
{
ctx.Error( "This node is not bound to a keyword, so the Off branch was used" );
ctx.Out( nameof( Out ), off );
return;
}
// Resolve the branch with the preprocessor when the backend can. Only the taken side is then
// compiled at all, so the other side's texture samples and loops cost nothing — as opposed to a
// run-time select, which evaluates both and throws one away.
if ( ctx is NodeEmitContext emit && emit.KeywordSelect( symbol, on, off ) is { IsValid: true } picked )
{
ctx.Out( nameof( Out ), picked );
return;
}
var condition = ctx.Helper( PrismParameters.ComboTest( symbol,
PrismParameters.SlangComboSymbol( keyword ) ) );
if ( !condition.IsValid )
{
ctx.Out( nameof( Out ), off );
return;
}
ctx.Out( nameof( Out ), ctx.Select( condition, on, off ) );
}
}
/// <summary>
/// Reads a multi-valued keyword as an integer, for a combo used as a mode selector rather than a
/// toggle.
/// </summary>
[NodeInfo( Id = "prism.parameter.keywordValue", Title = "Keyword Value", Category = "Parameter",
Icon = "list", Tier = NodeTier.Advanced,
Keywords = new[] { "keyword", "combo", "value", "mode", "enum" },
Description = "The integer a graph keyword currently holds. Compare it, or feed it to a switch." )]
[NodeVersion( 1 )]
public sealed class KeywordValueNode : PrismNode
{
/// <summary>
/// Which keyword to read. Typed as <see cref="ParamId"/> per addendum B4 so the clipboard can
/// renumber it; written to disk as the bare id string.
/// </summary>
public ParamId Keyword { get; set; }
/// <summary>The keyword's value.</summary>
[Out( "int", Name = "Value" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
if ( ctx is null ) return;
if ( PrismParameters.FindKeyword( ctx.Graph, Keyword ) is not null ) return;
ctx.Error( Keyword.IsValid
? $"The graph has no keyword '{Keyword}' any more"
: "This node is not bound to a keyword", null, DiagnosticCode.UnresolvedParameter );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var keyword = PrismParameters.FindKeyword( ctx.Graph, Keyword );
var symbol = PrismParameters.ComboSymbol( keyword );
if ( string.IsNullOrEmpty( symbol ) )
{
ctx.Error( "This node is not bound to a keyword" );
ctx.Out( nameof( Out ), ctx.Const( 0 ) );
return;
}
ctx.Out( nameof( Out ), ctx.Helper( PrismParameters.ComboValue( symbol,
PrismParameters.SlangComboSymbol( keyword ) ) ) );
}
}
/// <summary>
/// An author-time switch. The branch that loses is never traversed, so nothing behind it reaches the
/// shader at all — unlike a runtime branch, which compiles both sides.
/// </summary>
[NodeInfo( Id = "prism.parameter.staticSwitch", Title = "Static Switch", Category = "Parameter",
Icon = "toggle_on", Keywords = new[] { "static", "switch", "toggle", "variant", "compile" },
Description = "Picks one of two inputs while authoring. Only the chosen branch is compiled." )]
[NodeVersion( 1 )]
public sealed class StaticSwitchNode : PrismNode
{
/// <summary>Which branch wins.</summary>
public bool Value { get; set; } = true;
/// <summary>The value used while the switch is on.</summary>
[In( "T", Name = "On" )] public PortRef On { get; set; }
/// <summary>The value used while the switch is off.</summary>
[In( "T", Name = "Off" )] public PortRef Off { get; set; }
/// <summary>The chosen value.</summary>
[Out( "T", Name = "" )] public PortRef Out { get; set; }
/// <summary>The value used when <see cref="On"/> is unconnected.</summary>
[InlineValue( nameof( On ) )] public float DefaultOn { get; set; } = 1f;
/// <summary>The value used when <see cref="Off"/> is unconnected.</summary>
[InlineValue( nameof( Off ) )] public float DefaultOff { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.In( Value ? nameof( On ) : nameof( Off ) ) );
}
}
/// <summary>
/// Declares a named module-level constant and reads it back.
/// <para>
/// This is the readable form of a <c>#define</c>: the generated shader gets
/// <c>static const float3 g_vMyName = float3( … );</c> once, and every node that reads it refers to the
/// name rather than repeating the literal. Nothing about the compiled binary changes — it exists so
/// generated code stays legible and so a magic number has one place to live.
/// </para>
/// </summary>
[NodeInfo( Id = "prism.parameter.define", Title = "Shader Constant", Category = "Parameter",
Icon = "code", Tier = NodeTier.Advanced,
Keywords = new[] { "define", "constant", "static const", "macro", "named" },
Description = "A named module-level constant. Emitted once, referenced by name." )]
[NodeVersion( 1 )]
public sealed class ShaderConstantNode : PrismNode
{
PrismConstantKind _kind = PrismConstantKind.Float;
/// <summary>The constant's name, before the engine's hungarian prefix is applied.</summary>
public string Name { get; set; } = "Constant";
/// <summary>What shape the constant takes. Matrices are not supported here.</summary>
public PrismConstantKind Kind
{
get => _kind;
set
{
var kind = value == PrismConstantKind.Matrix ? PrismConstantKind.Float4 : value;
if ( _kind == kind ) return;
_kind = kind;
RebuildPorts();
}
}
/// <summary>The value, packed into four components. Unused components are ignored.</summary>
public Vector4 Value { get; set; }
/// <summary>The shader type this constant declares.</summary>
public ShaderType ValueType => _kind switch
{
PrismConstantKind.Float2 => ShaderType.Float2,
PrismConstantKind.Float3 => ShaderType.Float3,
PrismConstantKind.Float4 or PrismConstantKind.Color => ShaderType.Float4,
PrismConstantKind.Bool => ShaderType.Bool,
PrismConstantKind.Int => ShaderType.Int,
_ => ShaderType.Float
};
/// <summary>The symbol the constant is declared under.</summary>
public string Symbol => GraphCompiler.SymbolPrefix( ValueType ) + Parameter.Sanitize( Name );
/// <inheritdoc/>
protected override void OnDefinePorts( PortBuilder b ) =>
b?.Output( "Out", ValueType.ToString(), string.Empty );
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
if ( ctx is null || !string.IsNullOrWhiteSpace( Name ) ) return;
ctx.Error( "A shader constant needs a name", null, DiagnosticCode.GlobalCollision );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var type = ValueType;
var declaration = new GlobalDecl( Symbol, type, GlobalKind.Constant )
{
Default = ConstValue.From( Value )
};
ctx.Out( "Out", ctx.Global( declaration ) );
}
}