Editor/ShaderGraphPlus/Compiler/GraphCompiler.cs

Editor-side GraphCompiler for ShaderGraphPlus, responsible for evaluating a node-based shader graph, resolving node inputs (including subgraphs and custom functions), collecting shader resources (textures, samplers, gradients, parameters), and generating HLSL shader code for vertex and pixel stages and preview builds.

File AccessExternal Download
using Editor;
using Microsoft.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text;
using ShaderGraphPlus.Nodes;

namespace ShaderGraphPlus;

public sealed partial class GraphCompiler
{
	public struct GraphIssue
	{
		public BaseNodePlus Node;
		public string Message;
		public bool IsWarning;
	}

	/// <summary>
	/// Current graph we're compiling
	/// </summary>
	public ShaderGraphPlus Graph { get; private set; }

	/// <summary>
	/// Current SubGraph
	/// </summary>
	private ShaderGraphPlus Subgraph = null;

	/// <summary>
	/// Current SubGraph node that we are in
	/// </summary>
	private SubgraphNode SubgraphNode = null;

	/// <summary>
	/// The loaded sub-graphs
	/// </summary>
	public List<ShaderGraphPlus> Subgraphs { get; private set; }

	private List<(SubgraphNode, ShaderGraphPlus)> SubgraphStack = new();

	public Dictionary<string, string> CompiledTextures { get; private set; } = new();

	/// <summary>
	/// Pixel Shader Stage Includes
	/// </summary>
	public HashSet<string> PixelIncludes { get; private set; } = new();

	/// <summary>
	/// Vertex Shader Stage Includes
	/// </summary>
	public HashSet<string> VertexIncludes { get; private set; } = new();

	public Dictionary<string, string> PreviewImages = new();

	/// <summary>
	/// Vertex Shader stage inputs
	/// </summary>
	public Dictionary<string, string> VertexInputs { get; private set; } = new();

	/// <summary>
	/// Pixel Shader stage inputs
	/// </summary>
	public Dictionary<string, string> PixelInputs { get; private set; } = new();

	public Dictionary<string, string> ShaderDefines { get; private set; } = new();

	public int VoidLocalCount { get; set; } = 0;

	/// <summary>
	/// Is this compile for just the preview or not, preview uses attributes for constant values
	/// </summary>
	public bool IsPreview { get; private set; }

	/// <summary>
	/// Is this compile for the final generated result
	/// </summary>
	public bool IsNotPreview => !IsPreview;

	private partial class CompileResult
	{
		public List<(NodeResult localResult, NodeResult funcResult)> Results = new();
		public Dictionary<NodeInput, NodeResult> InputResults = new();

		public Dictionary<string, Sampler> SamplerStates = new();
		public Dictionary<string, TextureInput> TextureInputs = new();
		public Dictionary<string, Gradient> Gradients = new();
		public Dictionary<string, (string Options, NodeResult Result)> Parameters = new();
		public Dictionary<string, string> Globals { get; private set; } = new();
		public Dictionary<string, object> Attributes { get; private set; } = new();
		public HashSet<string> Functions { get; private set; } = new();

		public string RepresentativeTexture { get; set; }

		internal void Replace( Dictionary<string, string> globals, Dictionary<string, object> attributes, HashSet<string> functions )
		{
			Globals = globals;
			Attributes = attributes;
			Functions = functions;
		}
	}

	public enum ShaderStage
	{
		Vertex,
		Pixel,
	}

	/// <summary>
	/// Current shader stage being evaluated.
	/// </summary>
	public ShaderStage Stage { get; private set; }

	public bool IsVs => Stage == ShaderStage.Vertex;
	public bool IsPs => Stage == ShaderStage.Pixel;
	private string StageName => Stage == ShaderStage.Vertex ? "vs" : "ps";
	private string CurrentResultInput;

	private CompileResult VertexResult { get; set; } = new();
	private CompileResult PixelResult { get; set; } = new();
	private CompileResult ShaderResult => Stage == ShaderStage.Vertex ? VertexResult : PixelResult;

	public Action<string, object> OnAttribute { get; set; }

	// Init to 1, 0 is reserved.
	public int PreviewID { get; internal set; } = 1;

	private List<NodeInput> InputStack = new();

	private List<int> VoidDataHashes = new();

	private readonly Dictionary<BaseNodePlus, List<string>> NodeErrors = new();
	private readonly Dictionary<BaseNodePlus, List<string>> NodeWarnings = new();

	/// <summary>
	/// Error list.
	/// </summary>
	public IEnumerable<GraphIssue> Errors => NodeErrors
		.Select( x => new GraphIssue { Node = x.Key, Message = x.Value.FirstOrDefault(), IsWarning = false } );

	/// <summary>
	/// Warning list.
	/// </summary>
	public IEnumerable<GraphIssue> Warnings => NodeWarnings
		.Select( x => new GraphIssue { Node = x.Key, Message = x.Value.FirstOrDefault(), IsWarning = true } );

	public GraphCompiler( ShaderGraphPlus graph, Dictionary<string, ShaderFeatureBase> shaderFeatures, bool preview )
	{
		Graph = graph;
		IsPreview = preview;
		Stage = ShaderStage.Pixel;
		Subgraphs = new();
		AddSubgraphs( Graph );
		ShaderFeatures = shaderFeatures;

		// Set the Initial Vertex and Pixel stage inputs from ShaderTemplate.
		VertexInputs = ShaderTemplate.VertexInputs;
		PixelInputs = ShaderTemplate.PixelInputs;
	}

	public bool TryGetPreviewImage( string name, out string imagePath )
	{
		return PreviewImages.TryGetValue( name, out imagePath );
	}

	public void SetAttribute<T>( string name, T value )
	{
		// Dont even try to set an attribute of the type if it
		// isnt able to be set in the first place.
		if ( !ShaderAttributeTypes.Contains( value.GetType() ) )
		{
			return;
		}

		if ( !ShaderResult.Attributes.ContainsKey( name ) )
		{
			OnAttribute?.Invoke( name, value );
			ShaderResult.Attributes[name] = value;
		}
	}

	/// <summary>
	/// Generates and compiles a texture and returns the path to it.
	/// </summary>
	public string CompileTexture( TextureInput textureInput )
	{
		if ( string.IsNullOrWhiteSpace( textureInput.DefaultTexture ) )
			return "";

		var resourceText = string.Format( ShaderTemplate.TextureDefinition,
			textureInput.DefaultTexture,
			textureInput.ColorSpace,
			textureInput.ImageFormat,
			textureInput.Processor
		);

		var assetPath = $"shadergraphplus/textures/{textureInput.DefaultTexture.Replace( ".", "_" )}_shadergraphplus.generated.vtex";

		var resourcePath = Editor.FileSystem.Root.GetFullPath( "/.source2/temp" );
		resourcePath = System.IO.Path.Combine( resourcePath, assetPath );

		if ( AssetSystem.CompileResource( resourcePath, resourceText ) )
		{
			return assetPath;
		}
		else
		{
			SGPLogger.Warning( $"Failed to compile {textureInput.DefaultTexture}" );
			return "";
		}
	}

	private void AddSubgraphs( ShaderGraphPlus graph )
	{
		if ( graph != Graph )
		{
			if ( Subgraphs.Contains( graph ) )
				return;

			Subgraphs.Add( graph );
		}

		foreach ( var node in graph.Nodes )
		{
			if ( node is SubgraphNode subgraphNode )
			{
				AddSubgraphs( subgraphNode.Subgraph );
			}
		}
	}

	public void RegisterVertexInput( string type, string name, string semantic )
	{
		name = CleanName( name );

		if ( ShaderTemplate.InternalVertexInputs.ContainsKey( name ) )
		{
			SGPLogger.Error( $"VetexInput \"{name}\" is reserved by the GraphCompiler" );

			return;
		}

		var vertexInput = $"{type} {name} : {semantic};";

		if ( !VertexInputs.TryAdd( name, vertexInput ) )
		{
			SGPLogger.Warning( $"VertexInputs already contains key \"{name}\"", IsNotPreview );
		}
	}

	public void RegisterPixelInput( string type, string name, string semantic )
	{
		name = CleanName( name );

		if ( ShaderTemplate.InternalPixelInputs.ContainsKey( name ) )
		{
			SGPLogger.Error( $"PixelInput \"{name}\" is reserved by the GraphCompiler" );

			return;
		}

		var pixelInput = $"{type} {name} : {semantic};";

		if ( !PixelInputs.TryAdd( name, pixelInput ) )
		{
			SGPLogger.Warning( $"PixelInputs already contains key \"{name}\"", IsNotPreview );
		}
	}

	internal string RegisterCustomCode( string nodeID, string functionName, string functionInputs, Dictionary<string, string> outputResults, CustomCodeNodeMode mode )
	{
		if ( !ShaderResult.VoidFunctionLocals.ContainsKey( nodeID ) )
		{
			var outputData = new List<VoidFunctionResult>();
			var functionOutputsSb = new StringBuilder();

			foreach ( var output in outputResults.Index() )
			{
				var userAssignedname = output.Item.Key;
				var id = VoidLocalCount++;
				var compilerName = $"ol_{id}";

				functionOutputsSb.Append( (output.Index + 1) == outputResults.Count ? $"{compilerName}" : $" {compilerName}, " );

				VoidFunctionResult data = new(
					userAssignedname,
					compilerName,
					GetResultTypeFromHLSLDataType( output.Item.Value )
				);

				outputData.Add( data );
			}

			// Assemble the function call
			var funcCall = "";
			if ( mode == CustomCodeNodeMode.Generate )
			{
				funcCall = ResultHLSLFunction( functionName, $"{functionInputs}, {functionOutputsSb}" );
			}
			else if ( mode == CustomCodeNodeMode.File )
			{
				funcCall = $"{functionName}( {string.Join( ", ", $"{functionInputs}, {functionOutputsSb}" )} )";
			}
			else
			{
				throw new NotImplementedException( $"Unknown CustomFunction mode '{mode}'" );
			}

			var voidData = new VoidFunctionInfo(
				outputData,
				funcCall,
				SubgraphNode == null ? nodeID : SubgraphNode.Identifier,
				mode == CustomCodeNodeMode.File
			);

			ShaderResult.VoidFunctionLocals.Add( nodeID, voidData );

			return funcCall;
		}

		return ShaderResult.VoidFunctionLocals[nodeID].FunctionCall;
	}

	/// <summary>
	/// Register a hlsl shader include.
	/// </summary>
	/// <param name="path"></param>
	public void RegisterInclude( string path )
	{
		var list = IsVs ? VertexIncludes : PixelIncludes;

		if ( list.Contains( path ) )
			return;

		list.Add( path );
	}

	/// <summary>
	/// Register a hlsl define directive and then return the name of the define converted to all uppercase.
	/// </summary>
	public string RegisterDefine( string name, string value )
	{
		name = CleanName( name ).ToUpper();

		if ( ShaderDefines.ContainsKey( name ) )
			return name;

		ShaderDefines.Add( name, value );

		return name;
	}

	/// <summary>
	/// Register some generic global parameter for a node to use.
	/// </summary>
	public void RegisterGlobal( string name, string global )
	{
		var result = ShaderResult;
		if ( result.Globals.ContainsKey( name ) )
			return;

		result.Globals.Add( name, global );
	}

	/// <summary>
	/// Emit a scalar <c>float</c> expression. If <paramref name="r"/> is float2/float3/float4, uses swizzle
	/// so wiring a vector into a <c>float</c> HLSL parameter does not rely on implicit truncation (-Wconversion).
	/// </summary>
	public static string EmitScalarFloat( NodeResult r, float fallback )
	{
		if ( !r.IsValid || !r.IsFloatTypeResult() )
			return $"{fallback}";
		return r.Components > 1 ? r.Cast( 1 ) : r.Code;
	}

	public string ResultHLSLFunction( string name, params string[] args )
	{
		if ( !GraphHLSLFunctions.HasFunction( name ) )
			return null;

		var result = ShaderResult;
		if ( !result.Functions.Contains( name ) )
			result.Functions.Add( name );

		return $"{name}( {string.Join( ", ", args )} )";
	}

	public string RegisterHLSLFunction( string code, [CallerArgumentExpression( nameof( code ) )] string functionName = "", bool overrideFunction = false )
	{
		if ( !GraphHLSLFunctions.HasFunction( functionName ) || overrideFunction )
		{
			GraphHLSLFunctions.RegisterFunction( functionName, code, overrideFunction );
		}

		return functionName;
	}

	/// <summary>
	/// Loops through ShaderResult.Gradients to find the matching key then returns the corresponding Gradient.
	/// </summary>
	public Gradient GetGradient( string gradientName )
	{
		var result = ShaderResult;

		Gradient searchResult = new();

		foreach ( var gradient in result.Gradients )
		{
			if ( gradient.Key == gradientName )
			{
				searchResult = gradient.Value;
			}
		}

		return searchResult;
	}

	/// <summary>
	/// Register a gradient and return the name of the graident. A generic name is returned instead if the gradient name is empty.
	/// </summary>
	public string RegisterGradient( Gradient gradient, string gradientName )
	{
		var result = ShaderResult;

		var name = !string.IsNullOrWhiteSpace( gradientName ) ? CleanName( gradientName ) : $"Gradient_{result.Gradients.Count}";

		if ( !result.Gradients.ContainsKey( name ) )
		{
			result.Gradients.Add( name, gradient );
		}

		return name;
	}

	/// <summary>
	/// Register a sampler and return the name of it
	/// </summary>
	public string ResultSampler( string name, Sampler sampler )
	{
		name = !string.IsNullOrWhiteSpace( name ) ? CleanName( name ) : $"Sampler_{sampler.GetHashCode():X8}";

		if ( IsPreview )
		{
			return ResultValue( sampler, $"g_s{name}", previewNameOverride: true ).Code;
		}
		else
		{
			if ( !ShaderResult.SamplerStates.ContainsKey( name ) )
			{
				ShaderResult.SamplerStates.Add( name, sampler );
			}

			return $"g_s{name}";
		}
	}

	/// <summary>
	/// Register a texture and return the global name of it
	/// </summary>
	public string ResultTexture( TextureInput input, Texture texture, bool storePreviewImage = false )
	{
		var name = CleanName( input.Name );
		var result = ShaderResult;
		var globalName = "";

		name = string.IsNullOrWhiteSpace( name ) ? $"Texture_{StageName}_{ShaderResult.TextureInputs.Count}" : name;

		if ( !result.TextureInputs.ContainsKey( name ) )
			result.TextureInputs.Add( name, input );

		if ( storePreviewImage )
		{
			if ( !PreviewImages.ContainsKey( $"g_t{name}" ) )
			{
				PreviewImages.Add( $"g_t{name}", input.DefaultTexture );
			}
		}

		if ( texture != null )
			SetAttribute( name, texture );

		globalName = $"g_t{name}";

		if ( CurrentResultInput == "Albedo" && !input.IsAttribute )
		{
			result.RepresentativeTexture = globalName;
		}

		return globalName;
	}

	/// <summary>
	/// Get result of a sampler input with an optional default sampelr value if it failed to resolve
	/// </summary>
	public string ResultSamplerOrDefault( NodeInput samplerInput, Sampler defaultSampler )
	{
		var resultSampler = Result( samplerInput );
		return resultSampler.IsValid && resultSampler.ResultType == ResultType.Sampler ? resultSampler.Code : ResultSampler( $"Sampler_{defaultSampler.GetHashCode():X8}", defaultSampler );
	}

	/// <summary>
	/// Get result of an input with an optional default value if it failed to resolve
	/// </summary>
	public NodeResult ResultOrDefault<T>( NodeInput input, T defaultValue )
	{
		var result = Result( input );
		return result.IsValid ? result : ResultValue( defaultValue );
	}

	/// <summary>
	/// Get result of an named reroute
	/// </summary>
	internal NodeResult ResultNamedReroute( string name )
	{
		var node = Graph.FindNamedRerouteDeclarationNode( name );

		if ( node != null )
		{
			var result = node.Result.Invoke( this );

			return result;
		}

		throw new Exception( $"Could not find named reroute with name \"{name}\"" );
	}

	/// <summary>
	/// Get result of an input
	/// </summary>
	public NodeResult Result( NodeInput input )
	{
		void InvalidNodeResult( ref BaseNodePlus node, NodeResult funcResult )
		{
			if ( funcResult.IsValid )
				return;

			if ( !NodeErrors.TryGetValue( node, out var errors ) )
			{
				errors = new();
				NodeErrors.Add( node, errors );
			}

			if ( funcResult.Errors is null || funcResult.Errors.Length == 0 )
			{
				errors.Add( $"Missing input" );
			}
			else
			{
				node.HasError = true;
				node.ErrorMessage = funcResult.Errors.FirstOrDefault();

				foreach ( var error in funcResult.Errors )
					errors.Add( error );
			}
		}

		void NodeError( ref BaseNodePlus node, string error )
		{
			node.HasError = true;
			node.ErrorMessage = error;

			NodeErrors[node] = [node.ErrorMessage];
		}

		if ( !input.IsValid )
			return default;

		BaseNodePlus node = null;
		if ( string.IsNullOrEmpty( input.Subgraph ) )
		{
			if ( Subgraph is not null )
			{
				var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) );

				return Result( new()
				{
					Identifier = input.Identifier,
					Output = input.Output,
					Subgraph = Subgraph.Path,
					SubgraphNode = nodeId
				} );
			}

			node = Graph.FindNode( input.Identifier );
		}
		else
		{
			var subgraph = Subgraphs.FirstOrDefault( x => x.Path == input.Subgraph );
			if ( subgraph is not null )
			{
				node = subgraph.FindNode( input.Identifier );
			}
		}

		if ( ShaderResult.InputResults.TryGetValue( input, out var result ) )
		{
			return result;
		}

		if ( node == null )
		{
			return default;
		}

		var nodeType = node.GetType();
		var property = nodeType.GetProperty( input.Output );

		if ( property == null )
		{
			// Search for alias
			var allProperties = nodeType.GetProperties();
			foreach ( var prop in allProperties )
			{
				var alias = prop.GetCustomAttribute<AliasAttribute>();
				if ( alias is null ) continue;
				foreach ( var al in alias.Value )
				{
					if ( al == input.Output )
					{
						property = prop;
						break;
					}
				}
				if ( property != null )
					break;
			}
		}

		object value = null;

		if ( node is not IRerouteNode && node is not CustomFunctionNode && InputStack.Contains( input ) )
		{
			NodeErrors[node] = new List<string> { "Circular reference detected" };
			return default;
		}

		InputStack.Add( input );

		if ( Subgraph is not null && node.Graph != Subgraph )
		{
			if ( node.Graph != Graph )
			{
				Subgraph = node.Graph as ShaderGraphPlus;
			}
			else
			{
				Subgraph = null;
			}
		}

		if ( node is CustomFunctionNode customFunctionNode )
		{
			node.ClearError();

			if ( customFunctionNode.Mode == CustomCodeNodeMode.Generate || customFunctionNode.Mode == CustomCodeNodeMode.File )
			{
				var funcResult = customFunctionNode.GetResult( this );

				if ( !funcResult.IsValid )
				{
					InvalidNodeResult( ref node, funcResult );

					InputStack.Remove( input );
					return default;
				}

				if ( ShaderResult.VoidFunctionLocals.TryGetValue( node.Identifier, out VoidFunctionInfo data ) ) //&& !data.IsAlreadyPostProcessed )
				{
					funcResult.SetVoidLocalTargetID( node.Identifier );

					var userAssignedVariableName = input.Output;
					var compilerAssignedVariableName = data.GetCompilerAssignedName( userAssignedVariableName );

					var resultType = data.GetResultResultType( compilerAssignedVariableName );
					var localResult = new NodeResult( resultType, $"{compilerAssignedVariableName}", false, funcResult.Metadata );
					localResult.SetVoidLocalTargetID( funcResult.VoidLocalTargetID );

					if ( Subgraph != null )
					{
						if ( data.IsAlreadyPostProcessed )
						{
							InputStack.Remove( input );
							return localResult;
						}
					}
					else
					{
						// return the localResult if we are getting a result from a node that we have already evaluated. 
						foreach ( var inputResult in ShaderResult.InputResults )
						{
							if ( inputResult.Key.Identifier == input.Identifier )
							{
								InputStack.Remove( input );
								return localResult;
							}
						}
					}

					ShaderResult.InputResults.Add( input, localResult );
					ShaderResult.Results.Add( (localResult, funcResult) );
					ShaderResult.VoidFunctionLocals[node.Identifier] = data with { IsAlreadyPostProcessed = true };

					InputStack.Remove( input );

					return localResult;
				}


				SGPLogger.Error( "Failed to get localResult" );
			}
			else
			{
				throw new NotImplementedException( $"Unknown mode '{customFunctionNode.Mode}'" );
			}

		}

		if ( node is SubgraphNode subgraphNode )
		{
			var newStack = (subgraphNode, Subgraph);
			var lastNode = SubgraphNode;

			SubgraphStack.Add( newStack );
			Subgraph = subgraphNode.Subgraph;
			SubgraphNode = subgraphNode;

			if ( !Subgraphs.Contains( Subgraph ) )
			{
				Subgraphs.Add( Subgraph );
			}

			var resultNode = Subgraph.Nodes.OfType<SubgraphOutput>().Where( x => x.OutputName == input.Output ).FirstOrDefault();
			var resultInput = resultNode.Inputs.FirstOrDefault( x => x.Identifier == input.Output );
			if ( resultInput?.ConnectedOutput is not null )
			{
				var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) );
				var newConnection = new NodeInput()
				{
					Identifier = resultInput.ConnectedOutput.Node.Identifier,
					Output = resultInput.ConnectedOutput.Identifier,
					Subgraph = Subgraph.Path,
					SubgraphNode = nodeId
				};
				var newResult = Result( newConnection );

				if ( NodeErrors.Any() )
				{
					InputStack.Remove( input );
					return default;
				}

				InputStack.Remove( input );
				SubgraphStack.RemoveAt( SubgraphStack.Count - 1 );
				Subgraph = newStack.Item2;
				SubgraphNode = lastNode;

				return newResult;
			}
			else
			{
				//value = GetDefaultValue( subgraphNode, input.Output, resultInput.Type );

				switch ( resultNode.PortType )
				{
					case SubgraphPortType.Bool:
						value = false;
						break;
					case SubgraphPortType.Int:
						value = 1;
						break;
					case SubgraphPortType.Float:
						value = 1;
						break;
					case SubgraphPortType.Vector2:
						value = Vector2.One;
						break;
					case SubgraphPortType.Vector3:
						value = Vector3.One;
						break;
					case SubgraphPortType.Vector4:
						value = Vector4.One;
						break;
					case SubgraphPortType.Color:
						value = Color.White;
						break;
				}

				if ( value == null )
				{
					NodeError( ref node, $"Missing internal input \'{resultInput.DisplayInfo.Name}\' in node \'{Subgraph.Path}\'" );

					SGPLogger.Error( subgraphNode.ErrorMessage );

					return default;
				}
				else
				{
					SGPLogger.Error( $"Missing Internal Input \'{resultInput.DisplayInfo.Name}\' in node \'{Subgraph.Path}\' falling back to default value \'{value}\'" );
				}

				SubgraphStack.RemoveAt( SubgraphStack.Count - 1 );
				Subgraph = newStack.Item2;
				SubgraphNode = lastNode;
			}
		}
		else
		{
			if ( Subgraph is not null )
			{

				if ( node is SubgraphInput subgraphInput && !string.IsNullOrWhiteSpace( subgraphInput.Name ) )
				{
					var newResult = ResolveSubgraphInput( subgraphInput, ref value, out var error );

					if ( !string.IsNullOrWhiteSpace( error.ErrorString ) )
					{
						NodeErrors.Add( error.Node, new List<string> { error.ErrorString } );

						InputStack.Remove( input );
						return default;
					}

					if ( newResult.IsValid )
					{
						InputStack.Remove( input );
						return newResult;
					}
				}
			}
			else if ( Graph.IsSubgraph )
			{
				if ( node is SubgraphInput subgraphInput )
				{
					if ( subgraphInput.PreviewInput.IsValid )
					{
						var subgraphInputResult = Result( subgraphInput.PreviewInput );

						InputStack.Remove( input );
						return subgraphInputResult;
					}
				}
			}

			if ( value is null )
			{
				if ( property == null )
				{
					InputStack.Remove( input );
					return default;
				}

				value = property.GetValue( node );
			}

			if ( value == null )
			{
				InputStack.Remove( input );
				return default;
			}
		}

		if ( value is NodeResult nodeResult )
		{
			InputStack.Remove( input );
			return nodeResult;
		}
		else if ( value is NodeInput nodeInput )
		{
			if ( nodeInput == input )
			{
				InputStack.Remove( input );
				return default;
			}

			var newResult = Result( nodeInput );

			InputStack.Remove( input );
			return newResult;
		}
		else if ( value is NodeResult.Func resultFunc )
		{
			var funcResult = resultFunc.Invoke( this );

			if ( !funcResult.IsValid )
			{
				InvalidNodeResult( ref node, funcResult );

				InputStack.Remove( input );
				return default;
			}

			funcResult.SetVoidLocalTargetID( node.Identifier );

			if ( IsPreview )
			{
				funcResult.SetPreviewID( node.PreviewID );
				funcResult.ShouldPreview = node.CanPreview;
			}
			else
			{
				funcResult.ShouldPreview = false;
			}

			//if ( subgraphResult )
			//{
			//	funcResult.SetPreviewID( SubgraphNode.PreviewID );
			//}

			// We can return this result without making it a local variable because it's constant
			if ( funcResult.Constant )
			{
				InputStack.Remove( input );

				return funcResult;
			}

			int id = ShaderResult.InputResults.Count;
			var varName = $"l_{id}";
			var localResult = new NodeResult( funcResult.ResultType, varName, false, funcResult.Metadata );
			localResult.SetPreviewID( funcResult.PreviewID );
			localResult.SetVoidLocalTargetID( funcResult.VoidLocalTargetID );
			localResult.ShouldPreview = funcResult.ShouldPreview;

			if ( !ShaderResult.InputResults.ContainsKey( input ) )
			{
				ShaderResult.InputResults.Add( input, localResult );
			}

			ShaderResult.Results.Add( (localResult, funcResult) );

			InputStack.Remove( input );

			return localResult;
		}

		var resultVal = ResultValue( value );

		InputStack.Remove( input );

		return resultVal;
	}

	/// <summary>
	/// Get result of two inputs and cast to the largest component of the two (a float2 and float3 will both become float3 results)
	/// </summary>
	public (NodeResult, NodeResult) Result( NodeInput a, NodeInput b, float defaultA = 0.0f, float defaultB = 1.0f )
	{
		var resultA = ResultOrDefault( a, defaultA );
		var resultB = ResultOrDefault( b, defaultB );

		if ( resultA.Components == resultB.Components )
			return (resultA, resultB);

		if ( resultA.Components < resultB.Components )
			return (new( resultB.ResultType, resultA.Cast( resultB.Components ) ), resultB);

		return (resultA, new( resultA.ResultType, resultB.Cast( resultA.Components ) ));
	}

	private static object GetDefaultValue( SubgraphNode node, string name, Type type )
	{
		if ( !node.DefaultValues.TryGetValue( name, out var value ) )
		{
			switch ( type )
			{
				case Type t when t == typeof( bool ):
					return false;
				case Type t when t == typeof( int ):
					return 1;
				case Type t when t == typeof( float ):
					return 1.0f;
				case Type t when t == typeof( Vector2 ):
					return Vector2.One;
				case Type t when t == typeof( Vector3 ):
					return Vector3.One;
				case Type t when t == typeof( Vector4 ):
					return Vector4.One;
				case Type t when t == typeof( Color ):
					return Color.White;
				case Type t when t == typeof( Float2x2 ):
					return new Float2x2();
				case Type t when t == typeof( Float3x3 ):
					return new Float3x3();
				case Type t when t == typeof( Float4x4 ):
					return new Float4x4();
				case Type t when t == typeof( Sampler ):
					return new Sampler();
				case Type t when t == typeof( Texture ):
					var inputType = node.InputReferences.FirstOrDefault( x => x.Value.inputNode.Name == name ).Value.inputNode.PortType;

					return new TextureInput() { Type = (inputType == SubgraphPortType.Texture2DObject ? TextureType.Tex2D : TextureType.TexCube) };
			}

		}

		if ( value is JsonElement el )
		{
			if ( type == typeof( bool ) )
			{
				value = el.GetBoolean();
			}
			else if ( type == typeof( int ) )
			{
				value = el.GetInt32();
			}
			else if ( type == typeof( float ) )
			{
				value = el.GetSingle();
			}
			else if ( type == typeof( Vector2 ) )
			{
				value = Vector2.Parse( el.GetString() );
			}
			else if ( type == typeof( Vector3 ) )
			{
				value = Vector3.Parse( el.GetString() );
			}
			else if ( type == typeof( Vector4 ) )
			{
				value = Vector4.Parse( el.GetString() );
			}
			else if ( type == typeof( Color ) )
			{
				value = Color.Parse( el.GetString() ) ?? Color.White;
			}
			else if ( type == typeof( Float2x2 ) )
			{
				value = JsonSerializer.Deserialize<Float2x2>( el, ShaderGraphPlus.SerializerOptions() );
			}
			else if ( type == typeof( Float3x3 ) )
			{
				value = JsonSerializer.Deserialize<Float3x3>( el, ShaderGraphPlus.SerializerOptions() );
			}
			else if ( type == typeof( Float4x4 ) )
			{
				value = JsonSerializer.Deserialize<Float4x4>( el, ShaderGraphPlus.SerializerOptions() );
			}
			else if ( type == typeof( Sampler ) )
			{
				value = JsonSerializer.Deserialize<Sampler>( el, ShaderGraphPlus.SerializerOptions() );
			}
			else if ( type == typeof( Texture ) )
			{
				var inputType = node.InputReferences.FirstOrDefault( x => x.Value.inputNode.Name == name ).Value.inputNode.PortType;
				var textureInput = JsonSerializer.Deserialize<TextureInput>( el, ShaderGraphPlus.SerializerOptions() );

				value = textureInput with { Type = (inputType == SubgraphPortType.Texture2DObject ? TextureType.Tex2D : TextureType.TexCube) };
			}
		}

		return value;
	}

	/// <summary>
	/// Get result of a value that can be set in material editor
	/// </summary>
	public NodeResult ResultParameter<T>( T parameter ) where T : IBlackboardParameter
	{
		var name = parameter.Name;
		var value = parameter.GetValue();
		object min = default;
		object max = default;
		var isRange = false;
		var isAttribute = false;
		IParameterUI parameterUI = default;

		var priority = Graph.GetParameterIndex( parameter as BlackboardParameter );

		if ( parameter is IBlackboardMaterialParameter materialParameter )
		{
			isAttribute = materialParameter.IsAttribute;

			// Getting the Priority from the OrderedDictionary now :3
			var newUI = materialParameter.UI;
			newUI.Priority = priority;
			parameterUI = newUI;
		}

		if ( parameter is IGroupableBlackboardParameter groupableParameter )
		{
			if ( groupableParameter.IsGrouped )
			{
				if ( Graph.TryFindCategoryData( groupableParameter.GroupReference, out var category ) )
				{
					parameterUI.Priority = category.ParameterReferences.IndexOf( parameter.Identifier );
					parameterUI.PrimaryGroup = parameterUI.PrimaryGroup with
					{
						Name = category.Name,
						Priority = category.Priority
					};

					//Log.Info( $"TEST Category '{targetGroup.Name}' with Priorty '{parameterUI.PrimaryGroup.Priority}'" );
				}
			}
		}

		if ( parameter is IRangedBlackboardMaterialParameter rangedParameter )
		{
			min = rangedParameter.GetRangeMin();
			max = rangedParameter.GetRangeMax();
			isRange = min != max;
		}

		return ResultParameter( name, value, min, max, isRange, isAttribute, parameterUI );
	}

	/// <summary>
	/// Get result of a value that can be set in material editor
	/// </summary>
	public NodeResult ResultParameter<T>( string name, T value, T min = default, T max = default, bool isRange = false, bool isAttribute = false, IParameterUI ui = default )
	{
		if ( IsPreview || string.IsNullOrWhiteSpace( name ) || Subgraph is not null )
			return ResultValue( value );

		var attribName = name;
		name = CleanName( name );

		var prefix = value switch
		{
			bool _ => "g_b",
			int _ => "g_n",
			float _ => "g_fl",
			Vector2 _ => "g_v",
			Vector2Int _ => "g_v",
			Vector3 _ => "g_v",
			Vector3Int _ => "g_v",
			Vector4 _ => "g_v",
			Color _ => "g_v",
			Float2x2 _ => "g_m",
			Float3x3 _ => "g_m",
			Float4x4 _ => "g_m",
			Texture _ => "g_t",
			Sampler _ => "g_s",
			_ => throw new Exception( $"Unknown type \"{value.GetType()}\"" )
		};

		// Make sure the type T is can have a Default();
		bool canHaveDefualt = typeof( T ) switch
		{
			Type t when t == typeof( Float2x2 ) => false,
			Type t when t == typeof( Float3x3 ) => false,
			Type t when t == typeof( Float4x4 ) => false,
			_ => true,
		};

		if ( !name.StartsWith( prefix ) )
			name = prefix + name;

		if ( ShaderResult.Parameters.TryGetValue( name, out var parameter ) )
			return parameter.Result;

		parameter.Result = ResultValue( value, name );

		var options = new StringWriter();

		// If we're an attribute, we don't care about the UI options
		if ( isAttribute )
		{
			options.Write( $"Attribute( \"{attribName}\" ); " );

			if ( value is bool boolValue )
			{
				options.Write( $"Default( {(boolValue ? 1 : 0)} ); " );
			}
			else if ( canHaveDefualt )
			{
				options.Write( $"Default{parameter.Result.Components}( {value} );" );
			}
		}
		else if ( MaterialParameterTypes.Contains( value.GetType() ) )
		{
			if ( ui is FloatParameterUI floatParameterUI )
			{
				if ( floatParameterUI.Type != UIType.Default )
				{
					options.Write( $"UiType( {floatParameterUI.Type} ); " );
				}

				if ( floatParameterUI.Step > 0.0f )
				{
					options.Write( $"UiStep( {floatParameterUI.Step} ); " );
				}
			}
			else if ( value is int )
			{
				options.Write( $"UiType( Slider ); " );

			}
			else if ( value is bool )
			{
				options.Write( $"UiType( CheckBox ); " );
			}
			else if ( value is Color )
			{
				options.Write( $"UiType( Color ); " );
			}

			options.Write( $"UiGroup( \"{ui.UIGroup}\" ); " );

			if ( value is bool boolValue )
			{
				options.Write( $"Default( {(boolValue ? 1 : 0)} ); " );
			}
			else if ( canHaveDefualt )
			{
				options.Write( $"Default{parameter.Result.Components}( {value} ); " );
			}

			if ( value is not bool && parameter.Result.Components > 0 && isRange )
			{
				options.Write( $"Range{parameter.Result.Components}( {min}, {max} ); " );
			}
		}

		parameter.Options = options.ToString().Trim();

		// Avoid adding types that arnt exposed to the material editor.
		if ( MaterialParameterTypes.Contains( value.GetType() ) )
		{
			ShaderResult.Parameters.Add( name, parameter );
		}

		return parameter.Result;
	}

	/// <summary>
	/// Get result of a value, in preview mode an attribute will be registered and returned
	/// </summary>
	public NodeResult ResultValue<T>( T value, string name = null, bool previewOverride = false, bool previewNameOverride = false )
	{
		if ( value is NodeInput nodeInput ) return Result( nodeInput );

		bool isConstant = IsPreview && !previewOverride;
		bool isNamed = isConstant || !string.IsNullOrWhiteSpace( name );
		name = isConstant && !previewNameOverride ? $"g_{StageName}_{ShaderResult.Attributes.Count}" : name;

		if ( isConstant )
		{
			SetAttribute( name, value );
		}

		return value switch
		{
			bool v => isNamed ? new NodeResult( ResultType.Bool, $"{name}" ) : new NodeResult( ResultType.Bool, $"{v.ToString().ToLower()}" ) { },
			int v => isNamed ? new NodeResult( ResultType.Int, $"{name}" ) : new NodeResult( ResultType.Int, $"{v}", true ),
			float v => isNamed ? new NodeResult( ResultType.Float, $"{name}" ) : new NodeResult( ResultType.Float, $"{v}", true ),
			Vector2 v => isNamed ? new NodeResult( ResultType.Vector2, $"{name}" ) : new NodeResult( ResultType.Vector2, $"float2( {v.x}, {v.y} )" ),
			Vector3 v => isNamed ? new NodeResult( ResultType.Vector3, $"{name}" ) : new NodeResult( ResultType.Vector3, $"float3( {v.x}, {v.y}, {v.z} )" ),
			Vector4 v => isNamed ? new NodeResult( ResultType.Vector4, $"{name}" ) : new NodeResult( ResultType.Vector4, $"float4( {v.x}, {v.y}, {v.z}, {v.w} )" ),
			Color v => isNamed ? new NodeResult( ResultType.Vector4, $"{name}" ) : new NodeResult( ResultType.Vector4, $"float4( {v.r}, {v.g}, {v.b}, {v.a} )" ),
			Float2x2 v => isNamed ? new NodeResult( ResultType.Float2x2, $"{value}" ) : new NodeResult( ResultType.Float2x2, $"float2x2( {v.M11}, {v.M12}, {v.M21}, {v.M22} )" ),
			Float3x3 v => isNamed ? new NodeResult( ResultType.Float3x3, $"{value}" ) : new NodeResult( ResultType.Float3x3, $"float3x3( {v.M11}, {v.M12}, {v.M13}, {v.M21}, {v.M22}, {v.M23}, {v.M31}, {v.M32}, {v.M33} )" ),
			Float4x4 v => isNamed ? new NodeResult( ResultType.Float4x4, $"{value}" ) : new NodeResult( ResultType.Float4x4, $"float4x4( {v.M11}, {v.M12}, {v.M13}, {v.M14}, {v.M21}, {v.M22}, {v.M23}, {v.M24}, {v.M31}, {v.M32}, {v.M33}, {v.M34}, {v.M41}, {v.M42}, {v.M43}, {v.M44} )" ),
			Sampler v => new NodeResult( ResultType.Sampler, $"{name}", true ),
			_ => throw new ArgumentException( $"Unsupported attribute type `{value.GetType()}`" )
		};
	}

	private NodeResult ResolveSubgraphInput( SubgraphInput inputNode, ref object value, out (SubgraphNode Node, string ErrorString) error )
	{
		var lastStack = SubgraphStack.LastOrDefault();
		var lastNodeEntered = lastStack.Item1;
		error = new();

		if ( lastNodeEntered != null )
		{
			var parentInput = lastNodeEntered.InputReferences.FirstOrDefault( x => x.Key.Identifier == inputNode.Name );
			if ( parentInput.Key is not null )
			{
				var lastSubgraph = Subgraph;
				var lastNode = SubgraphNode;
				Subgraph = lastStack.Item2;
				SubgraphNode = (Subgraph is null) ? null : lastNodeEntered;
				SubgraphStack.RemoveAt( SubgraphStack.Count - 1 );

				var connectedPlug = parentInput.Key.ConnectedOutput;
				if ( connectedPlug is not null )
				{
					var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) );
					var newResult = Result( new()
					{
						Identifier = connectedPlug.Node.Identifier,
						Output = connectedPlug.Identifier,
						Subgraph = Subgraph?.Path,
						SubgraphNode = nodeId
					} );
					SubgraphStack.Add( lastStack );
					Subgraph = lastSubgraph;
					SubgraphNode = lastNode;
					return newResult;
				}
				else
				{
					value = GetDefaultValue( lastNodeEntered, inputNode.Name, parentInput.Value.inputNodeValueType );

					SubgraphStack.Add( lastStack );
					Subgraph = lastSubgraph;
					SubgraphNode = lastNode;

					var inputNodeInputType = parentInput.Value.inputNode.PortType;
					if ( inputNodeInputType == SubgraphPortType.Texture2DObject || inputNodeInputType == SubgraphPortType.TextureCubeObject )
					{
						var resultType = (inputNodeInputType == SubgraphPortType.Texture2DObject ? ResultType.Texture2D : ResultType.TextureCube);
						var textureInput = ((TextureInput)value) with { Type = (inputNodeInputType == SubgraphPortType.Texture2DObject ? TextureType.Tex2D : TextureType.TexCube) };
						var texurePath = CompileTexture( textureInput );
						var textureGlobal = ResultTexture( textureInput, Texture.Load( texurePath ), true );

						return new NodeResult( resultType, textureGlobal, true );
					}
					else if ( value is Sampler sampler )
					{
						var samplerResult = ResultSampler( $"Sampler_{sampler.GetHashCode():X8}", sampler );
						return new NodeResult( ResultType.Sampler, samplerResult, constant: true );
					}
				}
			}
		}

		return new();
	}

	// TODO : Fix this up
	/*
	public string GeneratePostProcessingComponent( PostProcessingComponentInfo postProcessiComponentInfo, string className, string shaderPath )
	{
		var ppcb = new PostProcessingComponentBuilder( postProcessiComponentInfo );
		var type = "";

		foreach ( var parameter in ShaderResult.Parameters )
		{
			type = parameter.Value.Result.ComponentType.ToSimpleString();

			if ( type is "System.Boolean" )
			{
				ppcb.AddBoolProperty( parameter.Key, parameter.Value.Options );
			}
			if ( type is "float" )
			{
				ppcb.AddFloatProperty( type, parameter.Key, parameter.Value.Options );
			}
			if ( type is "Vector2" )
			{
				ppcb.AddVector2Property( type, parameter.Key, parameter.Value.Options );
			}
			if ( type is "Vector3" )
			{
				ppcb.AddVector3Property( type, parameter.Key, parameter.Value.Options );
			}
			if ( type is "Vector4" )
			{
				ppcb.AddVector4Property( type, parameter.Key, parameter.Value.Options );
			}
			if ( type is "Color" )
			{
				ppcb.AddVector4Property( type, parameter.Key, parameter.Value.Options );
			}
		}

		return ppcb.Finish( className, shaderPath );
	}
	*/

	/// <summary>
	/// Generate shader code, will evaluate the graph if it hasn't already.
	/// Different code is generated for preview and not preview.
	/// </summary>
	public string Generate()
	{
		if ( Graph.IsSubgraph )
		{
			if ( !Graph.Nodes.OfType<SubgraphOutput>().Any() )
			{
				NodeErrors.Add( new DummyNode(), [$"There must be atleast one Subgraph Output node!"] );
			}
		}

		// May have already evaluated and there's errors
		if ( Errors.Any() )
			return null;

		/*
		if ( Graph.MaterialDomain == MaterialDomain.BlendingSurface )
		{
			VertexInputs.Add( "vColorBlendValues", "float4 vColorBlendValues : TEXCOORD4 < Semantic( VertexPaintBlendParams ); >;" );
			VertexInputs.Add( "vColorPaintValues", "float4 vColorPaintValues : TEXCOORD5 < Semantic( VertexPaintTintColor ); >;" );
		
			PixelInputs.Add( "vBlendValues", "float4 vBlendValues : TEXCOORD14;" );
			PixelInputs.Add( "vPaintValues", "float4 vPaintValues : TEXCOORD15;" );
		}
		*/

		// Pre-Register anything before we Generate anything. Shouldn't cause any issues i hope.
		foreach ( var node in Graph.Nodes.OfType<IPreRegisterNodeData>() )
		{
			node.PreRegister( this );
		}

		var material = GenerateMaterial();
		var pixelOutput = GeneratePixelOutput();

		// If we have any errors after evaluating, no point going further
		if ( Errors.Any() )
			return null;

		var shaderTemplate = ShaderTemplate.Code;

		if ( Graph.Domain is ShaderDomain.BlendingSurface )
		{
			shaderTemplate = ShaderTemplateBlending.Code;
		}

		return string.Format( shaderTemplate,
			Graph.Description, // {0}
			IndentString( GenerateFeatures(), 1 ), // {1}
			IndentString( GenerateCommon(), 1 ), // {2}
			IndentString( GenerateStageInputs( ShaderStage.Vertex ), 1 ), // {3}
			IndentString( GenerateStageInputs( ShaderStage.Pixel ), 1 ), // {4}
			IndentString( GenerateGlobals(), 1 ), // {5}
			IndentString( GenerateLocals(), 2 ), // {6}
			IndentString( material, 2 ), // {7}
			IndentString( GenerateVertex(), 2 ), // {8}
			IndentString( GenerateGlobals(), 1 ), // {9}
			IndentString( GenerateVertexComboRules(), 1 ), // {10}
			IndentString( GeneratePixelComboRules(), 1 ),  // {11}
			IndentString( GenerateFunctions( PixelResult ), 1 ),  // {12}
			IndentString( GenerateFunctions( VertexResult ), 1 ),  // {13}
			IndentString( GeneratePixelInit(), 2 ), // {14}
			IndentString( pixelOutput, 2 ) // {15}
		);
	}

	private string GenerateMaterial()
	{
		Stage = ShaderStage.Pixel;
		Subgraph = null;
		SubgraphStack.Clear();

		if ( Graph.ShadingModel != ShadingModel.Lit || Graph.Domain == ShaderDomain.PostProcess ) return "";

		var resultNode = Graph.Nodes.OfType<BaseResult>().FirstOrDefault();

		if ( resultNode == null )
			return null;

		var sb = new StringBuilder();
		var visited = new HashSet<string>();

		foreach ( var property in GetNodeInputProperties( resultNode.GetType() ) )
		{
			if ( property.Name == "PositionOffset" )
				continue;

			CurrentResultInput = property.Name;
			visited.Add( property.Name );

			NodeResult result;

			if ( property.GetValue( resultNode ) is NodeInput connection && connection.IsValid() )
			{
				result = Result( connection );
			}
			else
			{
				var editorAttribute = property.GetCustomAttribute<BaseNodePlus.NodeValueEditorAttribute>();
				if ( editorAttribute == null )
					continue;

				var valueProperty = resultNode.GetType().GetProperty( editorAttribute.ValueName );
				if ( valueProperty == null )
					continue;

				result = ResultValue( valueProperty.GetValue( resultNode ) );

			}

			if ( Errors.Any() )
				return null;

			if ( !result.IsValid() )
				continue;

			if ( string.IsNullOrWhiteSpace( result.Code ) )
				continue;

			var inputAttribute = property.GetCustomAttribute<BaseNodePlus.InputAttribute>();
			var componentCount = GetComponentCount( inputAttribute.Type );

			sb.AppendLine( $"m.{property.Name} = {result.Cast( componentCount )};" );
		}

		if ( Graph.IsSubgraph )
		{
			var subgraphOutputs = Graph.Nodes.OfType<SubgraphOutput>();
			var reservedPreview = new Dictionary<SubgraphOutputPreviewType, SubgraphOutput>();

			foreach ( var subgraphOutput in subgraphOutputs )
			{
				subgraphOutput.ClearError();

				if ( reservedPreview.ContainsKey( subgraphOutput.Preview ) )
				{
					subgraphOutput.HasError = true;
					subgraphOutput.ErrorMessage = $"SubgraphOutput \"{reservedPreview[subgraphOutput.Preview].OutputName}\" has already claimed the \"{subgraphOutput.Preview}\" preview type";

					NodeErrors.Add( subgraphOutput, [subgraphOutput.ErrorMessage] );
					continue;
				}

				if ( subgraphOutput.Preview == SubgraphOutputPreviewType.None )
					continue;

				reservedPreview.Add( subgraphOutput.Preview, subgraphOutput );

				subgraphOutput.AddMaterialOutput( this, sb, subgraphOutput.Preview, out var errors );

				if ( errors.Any() )
				{
					NodeErrors.Add( new DummyNode(), errors );

					return "";
				}
			}

			reservedPreview.Clear();
		}

		visited.Clear();
		CurrentResultInput = null;

		return sb.ToString();
	}

	private string GenerateVertex()
	{
		Stage = ShaderStage.Vertex;

		var resultNode = Graph.Nodes.OfType<BaseResult>().FirstOrDefault();
		if ( resultNode == null )
			return null;

		var positionOffsetInput = resultNode.GetPositionOffset();

		var sb = new StringBuilder();
		var sb2 = new StringBuilder();

		foreach ( var vertexInput in VertexInputs )
		{
			sb2.AppendLine( $"i.{vertexInput.Key} = v.{vertexInput.Key};" );
		}

		switch ( Graph.Domain )
		{
			case ShaderDomain.Surface:
				sb.AppendLine( $@"
PixelInput i = ProcessVertex( v );
i.vPositionOs = v.vPositionOs.xyz;

{sb2.ToString()}
ExtraShaderData_t extraShaderData = GetExtraPerInstanceShaderData( v.nInstanceTransformID );
i.vTintColor = extraShaderData.vTint;

VS_DecodeObjectSpaceNormalAndTangent( v, i.vNormalOs, i.vTangentUOs_flTangentVSign );
		" );
				break;
			case ShaderDomain.BlendingSurface:
				sb.AppendLine( $@"
PixelInput i = ProcessVertex( v );

{sb2.ToString()}
i.vBlendValues = v.vColorBlendValues;
i.vPaintValues = v.vColorPaintValues;
" );
				break;
			case ShaderDomain.PostProcess:
				sb.AppendLine( $@"
PixelInput i;

{sb2.ToString()}
i.vPositionPs = float4( v.vPositionOs.xy, 0.0f, 1.0f );
i.vPositionWs = float3( v.vTexCoord, 0.0f );
" );
				break;
		}

		NodeResult result;

		if ( positionOffsetInput is NodeInput connection && connection.IsValid() )
		{
			result = Result( connection );

			if ( !Errors.Any() && result.IsValid() && !string.IsNullOrWhiteSpace( result.Code ) )
			{
				var componentCount = GetComponentCount( typeof( Vector3 ) );

				GenerateLocalResults( ref sb, ShaderResult.Results, out _, true, 0 );

				sb.AppendLine( $"i.vPositionWs.xyz += {result.Cast( componentCount )};" );
				sb.AppendLine( "i.vPositionPs.xyzw = Position3WsToPs( i.vPositionWs.xyz );" );
			}
		}

		switch ( Graph.Domain )
		{
			case ShaderDomain.Surface:
				sb.AppendLine( "return FinalizeVertex( i );" );
				break;
			case ShaderDomain.BlendingSurface:
				sb.AppendLine( "return FinalizeVertex( i );" );
				break;
			case ShaderDomain.PostProcess:
				sb.AppendLine( "return i;" );
				break;
		}
		return sb.ToString();
	}

	private string GeneratePixelOutput()
	{
		Stage = ShaderStage.Pixel;

		Subgraph = null;
		SubgraphStack.Clear();

		if ( Graph.ShadingModel == ShadingModel.Unlit || Graph.Domain == ShaderDomain.PostProcess )
		{
			var resultNode = Graph.Nodes.OfType<BaseResult>().FirstOrDefault();
			if ( resultNode == null )
				return null;

			var albedoResult = resultNode.GetAlbedoResult( this );
			string albedo = "float3( 1.0f, 1.0f, 1.0f )";
			if ( albedoResult.IsValid )
			{
				albedo = albedoResult.Cast( GetComponentCount( typeof( Vector3 ) ) ) ?? "float3( 1.0f, 1.0f, 1.0f )";
			}

			var opacityResult = resultNode.GetOpacityResult( this );
			string opacity = "1.0f";
			if ( opacityResult.IsValid )
			{
				opacity = opacityResult.Cast( 1 ) ?? "1.0f";
			}

			return $"return float4( {albedo}, {opacity} );";
		}
		else if ( Graph.ShadingModel == ShadingModel.Lit )
		{
			return ShaderTemplate.Material_output;
		}

		return null;
	}

	private string GenerateFeatures()
	{
		var sb = new StringBuilder();
		var result = ShaderResult;

		sb.AppendLine( "#include \"common/features.hlsl\"" );

		// Register any Graph level Shader Features...
		//RegisterShaderFeatures( Graph.shaderFeatureNodeResults );

		if ( Graph.Domain is ShaderDomain.BlendingSurface )
		{
			sb.AppendLine( "Feature( F_MULTIBLEND, 0..3 ( 0=\"1 Layers\", 1=\"2 Layers\", 2=\"3 Layers\", 3=\"4 Layers\", 4=\"5 Layers\" ), \"Number Of Blendable Layers\" );" );
		}

		foreach ( var feature in ShaderFeatures )
		{
			if ( feature.Value is ShaderFeatureBoolean boolFeature )
			{
				sb.AppendLine( $"Feature( {boolFeature.GetFeatureString()}, 0..1, \"{boolFeature.HeaderName}\" );" );
			}
			else if ( feature.Value is ShaderFeatureEnum enumFeature )
			{
				var optionsBody = BuildFeatureOptionsBody( enumFeature.Options );
				sb.AppendLine( $"Feature( {enumFeature.GetFeatureString()}, 0..{enumFeature.Options.Count - 1} ( {optionsBody} ), \"{enumFeature.HeaderName}\" );" );
			}
		}

		// TODO :
		//if ( Graph.FeatureRules.Any() )
		//{
		//	foreach ( var rule in Graph.FeatureRules )
		//	{
		//		if ( rule.IsValid )
		//		{
		//			sb.AppendLine( $"FeatureRule(Allow1( {String.Join( ", ", rule.Features )} ), \"{rule.HoverHint}\");" );
		//		}
		//	}
		//}

		return sb.ToString();
	}

	private string GenerateCommon()
	{
		var sb = new StringBuilder();

		if ( IsPreview )
		{
			sb.AppendLine( $"#ifndef SHADERGRAPHPLUS_PREVIEW" );
			sb.AppendLine( IndentString( $"#define SHADERGRAPHPLUS_PREVIEW", 1 ) );
			sb.AppendLine( $"#endif" );
		}

		if ( ShaderFeatures.Any() )
		{
			sb.AppendLine( $"#ifndef SWITCH_TRUE" );
			sb.AppendLine( IndentString( $"#define SWITCH_TRUE 1", 1 ) );
			sb.AppendLine( $"#endif" );

			sb.AppendLine( $"#ifndef SWITCH_FALSE" );
			sb.AppendLine( IndentString( $"#define SWITCH_FALSE 0", 1 ) );
			sb.AppendLine( $"#endif" );

			sb.AppendLine();
		}

		var blendMode = Graph.BlendMode;
		var alphaTest = blendMode == BlendMode.Masked ? 1 : 0;
		var translucent = blendMode == BlendMode.Translucent ? 1 : 0;

		sb.AppendLine( $"#ifndef S_ALPHA_TEST" );
		sb.AppendLine( IndentString( $"#define S_ALPHA_TEST {alphaTest}", 1 ) );
		sb.AppendLine( $"#endif" );

		sb.AppendLine( $"#ifndef S_TRANSLUCENT" );
		sb.AppendLine( IndentString( $"#define S_TRANSLUCENT {translucent}", 1 ) );
		sb.AppendLine( $"#endif" );

		sb.AppendLine();

		sb.AppendLine( $"#include \"common/shared.hlsl\"" );
		sb.AppendLine( $"#include \"common/gradient.hlsl\"" );
		sb.AppendLine( $"#include \"procedural.hlsl\"" );

		sb.AppendLine();

		sb.Append( $"#define S_UV2 1" );

		if ( ShaderDefines.Any() )
		{
			sb.AppendLine();
			sb.AppendLine();

			foreach ( var define in ShaderDefines )
			{
				sb.AppendLine( $"#ifndef {define.Key}" );
				sb.AppendLine( IndentString( $"#define {define.Key} {define.Value}", 1 ) );
				sb.AppendLine( $"#endif" );
			}
		}

		return sb.ToString();
	}

	private string GenerateStageInputs( ShaderStage shaderStage )
	{
		var sb = new StringBuilder();

		if ( shaderStage == ShaderStage.Vertex )
		{
			sb.AppendLine();

			foreach ( var vertexInput in VertexInputs )
			{
				if ( !ShaderTemplate.InternalVertexInputs.ContainsValue( vertexInput.Value ) )
				{
					sb.AppendLine( vertexInput.Value );
				}
			}
		}
		else
		{
			sb.AppendLine();

			foreach ( var pixelInput in PixelInputs )
			{
				if ( !ShaderTemplate.InternalPixelInputs.ContainsValue( pixelInput.Value ) )
				{
					sb.AppendLine( pixelInput.Value );
				}
			}
		}

		return sb.ToString();
	}

	private string GenerateGlobals()
	{
		var sb = new StringBuilder();

		var includes = IsVs ? VertexIncludes : PixelIncludes;

		if ( IsPs && Graph.Domain == ShaderDomain.PostProcess )
		{
			sb.AppendLine( "#include \"postprocess/functions.hlsl\"" );
			sb.AppendLine( "#include \"postprocess/common.hlsl\"" );
		}

		foreach ( var include in includes )
		{
			sb.AppendLine( $"#include \"{include}\"" );
		}

		if ( includes.Any() )
		{
			sb.AppendLine();
		}

		foreach ( var global in ShaderResult.Globals )
		{
			sb.AppendLine( global.Value );
		}

		foreach ( var feature in ShaderFeatures )
		{
			if ( IsPreview )
			{
				sb.AppendLine( $"DynamicCombo( {feature.Value.GetDynamicComboString()}, {feature.Value.GetOptionRangeString()}, Sys( ALL ) );" );
			}
			else
			{
				sb.AppendLine( $"StaticCombo( {feature.Value.GetStaticComboString()}, {feature.Value.GetFeatureString()}, Sys( ALL ) );" );
			}

			sb.AppendLine();
		}

		// Support for color buffer in post-process shaders
		if ( IsPs && Graph.Domain is ShaderDomain.PostProcess )
		{
			sb.AppendLine( "Texture2D g_tColorBuffer < Attribute( \"ColorBuffer\" ); SrgbRead ( true ); >;" );
		}

		if ( IsPreview )
		{
			foreach ( var result in ShaderResult.TextureInputs )
			{
				sb.Append( $"{result.Value.CreateTexture( result.Key )} <" )
				  .Append( $" Attribute( \"{result.Key}\" );" )
				  .Append( $" SrgbRead( {result.Value.SrgbRead} ); >;" )
				  .AppendLine();
			}

			foreach ( var result in ShaderResult.Attributes )
			{
				if ( result.Value is Texture || result.Value == null || !ShaderAttributeTypes.Contains( result.Value.GetType() ) )
					continue;

				var typeName = result.Value switch
				{
					bool _ => "bool",
					int _ => "int",
					float _ => "float",
					Vector2 _ => "float2",
					Vector3 _ => "float3",
					Vector4 _ or Color _ => "float4",
					Float2x2 _ => "float2x2",
					Float3x3 _ => "float3x3",
					Float4x4 _ => "float4x4",
					Sampler _ => "SamplerState",
					_ => null
				};

				sb.AppendLine( $"{typeName} {result.Key} < Attribute( \"{result.Key}\" ); >;" );
			}

			sb.AppendLine( "float g_flPreviewTime < Attribute( \"g_flPreviewTime\" ); >;" );
			sb.AppendLine( $"int g_iStageId < Attribute( \"g_iStageId\" ); >;" );
		}
		else
		{
			foreach ( var sampler in ShaderResult.SamplerStates )
			{
				if ( sampler.Value.IsAttribute )
				{
					sb.AppendLine( $"SamplerState g_s{sampler.Key} < Attribute( \"{sampler.Key}\" ); >;" );
				}
				else
				{
					sb.Append( $"SamplerState g_s{sampler.Key} <" )
					  .Append( $" Filter( {sampler.Value.Filter.ToString().ToUpper()} );" )
					  .Append( $" AddressU( {sampler.Value.AddressModeU.ToString().ToUpper()} );" )
					  .Append( $" AddressV( {sampler.Value.AddressModeV.ToString().ToUpper()} );" )
					  .Append( $" AddressW( {sampler.Value.AddressModeW.ToString().ToUpper()} );" )
					  .Append( $" MaxAniso( {sampler.Value.MaxAnisotropy.ToString()} ); >;" )
					  .AppendLine();
				}
			}

			foreach ( var result in ShaderResult.TextureInputs )
			{
				// If we're an attribute, we don't care about texture inputs
				if ( result.Value.IsAttribute )
					continue;

				var defaultTex = result.Value.DefaultTexture;
				sb.Append( $"{result.Value.CreateInput}( {result.Key}, {result.Value.ColorSpace}, 8," )
				  .Append( $" \"{result.Value.Processor.ToString()}\"," )
				  .Append( $" \"_{result.Value.ExtensionString.ToLower()}\"," )
				  .Append( $" \"{result.Value.UIGroup}\"," )
				  .Append( string.IsNullOrEmpty( defaultTex )
							? $" Default4( {result.Value.DefaultColor} ) );"
							: $" DefaultFile( \"{defaultTex}\" ) );" )
				  .AppendLine();
			}

			foreach ( var result in ShaderResult.TextureInputs )
			{
				// If we're an attribute, we don't care about the UI options
				if ( result.Value.IsAttribute )
				{
					sb.AppendLine( $"{result.Value.CreateTexture( result.Key )} < Attribute( \"{result.Key}\" ); >;" );
				}
				else
				{
					sb.Append( $"{result.Value.CreateTexture( result.Key )} < Channel( RGBA, Box( {result.Key} ), {(result.Value.SrgbRead ? "Srgb" : "Linear")} );" )
					  .Append( $" OutputFormat( {result.Value.ImageFormat} );" )
					  .Append( $" SrgbRead( {result.Value.SrgbRead} ); >;" )
					  .AppendLine();
				}
			}

			if ( !string.IsNullOrWhiteSpace( ShaderResult.RepresentativeTexture ) )
			{
				sb.AppendLine( $"TextureAttribute( LightSim_DiffuseAlbedoTexture, {ShaderResult.RepresentativeTexture} )" );
				sb.AppendLine( $"TextureAttribute( RepresentativeTexture, {ShaderResult.RepresentativeTexture} )" );
			}

			foreach ( var parameter in ShaderResult.Parameters )
			{
				sb.AppendLine( $"{parameter.Value.Result.TypeName} {parameter.Key} < {parameter.Value.Options} >;" );
			}
		}

		if ( sb.Length > 0 )
		{
			sb.Insert( 0, "\n" );
		}

		return sb.ToString();
	}

	private string GenerateLocals( bool noPreviewOverride = false )
	{
		var sb = new StringBuilder();

		if ( ShaderResult.Results.Any() )
		{
			sb.AppendLine();
		}

		foreach ( var gradient in ShaderResult.Gradients )
		{
			sb.AppendLine( $"Gradient {gradient.Key} = Gradient::Init();" );
			sb.AppendLine();

			var colorindex = 0;
			var alphaindex = 0;

			sb.AppendLine( $"{gradient.Key}.colorsLength = {gradient.Value.Colors.Count};" );
			sb.AppendLine( $"{gradient.Key}.alphasLength = {gradient.Value.Alphas.Count};" );

			foreach ( var color in gradient.Value.Colors )
			{
				if ( ConCommands.VerboseDebgging )
				{
					SGPLogger.Info( $"{gradient.Key} Gradient Color {colorindex} : {color.Value} Time : {color.Time}" );
				}

				// All good with time as the 4th component?
				sb.AppendLine( $"{gradient.Key}.colors[{colorindex++}] = float4( {color.Value.r}, {color.Value.g}, {color.Value.b}, {color.Time} );" );
			}

			foreach ( var alpha in gradient.Value.Alphas )
			{
				sb.AppendLine( $"{gradient.Key}.alphas[{alphaindex++}] = float( {alpha.Value} );" );
			}

			sb.AppendLine();
		}

		GenerateLocalResults( ref sb, ShaderResult.Results, out _, noPreviewOverride, 0 );

		return sb.ToString();
	}

	private void GenerateLocalResults( ref StringBuilder sb, IEnumerable<(NodeResult localResult, NodeResult funcResult)> shaderResults, out (NodeResult localResult, NodeResult funcResult) lastResult, bool noPreviewOverride = false, int indentLevel = 0 )
	{
		lastResult = (new NodeResult(), new NodeResult());

		foreach ( var result in shaderResults )
		{
			lastResult = result;
			var comboBodyStr = "";

			if ( result.funcResult.TryGetMetaData<string>( nameof( MetadataType.ComboSwitchBody ), out var comboSwitchBody ) )
			{
				comboBodyStr = comboSwitchBody;

				if ( !string.IsNullOrWhiteSpace( comboSwitchBody ) )
				{
					sb.AppendLine( IndentString( comboSwitchBody, indentLevel ) );
				}
			}

			// CustomFunction node uses this primarly.
			if ( ShaderResult.VoidFunctionLocals.Any() && ShaderResult.VoidFunctionLocals.ContainsKey( result.funcResult.VoidLocalTargetID ) )
			{
				var data = ShaderResult.VoidFunctionLocals[result.funcResult.VoidLocalTargetID];
				sb.AppendLine();

				if ( !VoidDataHashes.Contains( data.GetHashCode() ) )
				{
					VoidDataHashes.Add( data.GetHashCode() );

					// Init all the output results.
					foreach ( var outResult in data.TargetResults )
					{
						sb.AppendLine( IndentString( InitializeVariable( outResult.ResultType, outResult.CompilerAssignedName ), indentLevel ) );
					}

					sb.AppendLine( IndentString( $"{data.FunctionCall};", indentLevel ) );
					sb.AppendLine();
				}
			}

			if ( result.funcResult.ResultType != ResultType.VoidFunction )
			{
				if ( result.funcResult.ResultType == ResultType.Float2x2 ||
					 result.funcResult.ResultType == ResultType.Float3x3 ||
					 result.funcResult.ResultType == ResultType.Float4x4 )
				{
					if ( IsPreview )
					{
						sb.AppendLine( IndentString( $"{result.funcResult.TypeName} {result.localResult} = {result.funcResult.TypeName}( {result.funcResult.Code} );", indentLevel ) );
					}
					else
					{
						sb.AppendLine( IndentString( $"{result.funcResult.TypeName} {result.localResult} = {result.funcResult.Code};", indentLevel ) );
					}
				}
				else
				{
					sb.AppendLine( IndentString( $"{result.funcResult.TypeName} {result.localResult} = {result.funcResult.Code};", indentLevel ) );
				}
			}

			if ( !noPreviewOverride )
			{
				if ( IsPs && IsPreview && string.IsNullOrWhiteSpace( comboBodyStr ) )
				{
					if ( result.localResult.ResultType == ResultType.Bool )
					{
						// TODO
					}
					else if ( result.localResult.CanPreview && result.localResult.ShouldPreview && result.localResult.PreviewID != ShaderGraphPlusGlobals.GraphCompiler.NoNodePreviewID )
					{
						sb.AppendLine( IndentString( $"if ( g_iStageId == {result.localResult.PreviewID} ) return {result.localResult.Cast( 4, 1.0f )};", indentLevel ) );
					}
				}
			}
		}
	}

	private string GenerateVertexComboRules()
	{
		var sb = new StringBuilder();

		if ( IsNotPreview )
			return sb.ToString();

		sb.AppendLine();
		return sb.ToString();
	}

	private string GeneratePixelComboRules()
	{
		var sb = new StringBuilder();

		if ( !IsNotPreview )
		{
			sb.AppendLine();
			sb.AppendLine( "DynamicCombo( D_RENDER_BACKFACES, 0..1, Sys( ALL ) );" );
			if ( Graph.RenderFace == RenderFace.Front )
				sb.AppendLine( "RenderState( CullMode, D_RENDER_BACKFACES ? NONE : BACK );" );
			else if ( Graph.RenderFace == RenderFace.Back )
				sb.AppendLine( "RenderState( CullMode, D_RENDER_BACKFACES ? NONE : FRONT );" );
			else
				sb.AppendLine( "RenderState( CullMode, NONE );" );
		}
		else
		{
			sb.AppendLine();
			if ( Graph.RenderFace == RenderFace.Front )
				sb.AppendLine( "RenderState( CullMode, F_RENDER_BACKFACES ? NONE : BACK );" );
			else if ( Graph.RenderFace == RenderFace.Back )
				sb.AppendLine( "RenderState( CullMode, F_RENDER_BACKFACES ? NONE : FRONT );" );
			else
				sb.AppendLine( "RenderState( CullMode, NONE );" );
		}

		return sb.ToString();
	}

	private string GeneratePixelInit()
	{
		Stage = ShaderStage.Pixel;
		if ( Graph.ShadingModel == ShadingModel.Lit && Graph.Domain != ShaderDomain.PostProcess )
			return ShaderTemplate.Material_init;
		return "";
	}

	public static string IndentString( string input, int tabCount )
	{
		if ( tabCount == 0 ) return input;

		if ( string.IsNullOrWhiteSpace( input ) )
			return input;

		var tabs = new string( '\t', tabCount );
		var lines = input.Split( '\n' );

		for ( int i = 0; i < lines.Length; i++ )
		{
			lines[i] = tabs + lines[i];
		}

		return string.Join( "\n", lines );
	}

	public static string CleanName( string name )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			return "";

		name = name.Trim();
		name = new string( name.Where( x => char.IsLetter( x ) || char.IsNumber( x ) || x == '_' ).ToArray() );

		return name;
	}

	private static int GetComponentCount( Type inputType )
	{
		return inputType switch
		{
			Type t when t == typeof( int ) => 1,
			Type t when t == typeof( float ) => 1,
			Type t when t == typeof( Vector2 ) => 2,
			Type t when t == typeof( Vector3 ) => 3,
			Type t when t == typeof( Vector4 ) || t == typeof( Color ) => 4,
			Type t when t == typeof( Float2x2 ) => 4,
			Type t when t == typeof( Float3x3 ) => 6,
			Type t when t == typeof( Float4x4 ) => 16,
			_ => 0
		};
	}

	private static string InitializeVariable( ResultType resultType, string name )
	{
		switch ( resultType )
		{
			case ResultType.Bool:
				return $"bool {name} = false;";
			case ResultType.Int:
				return $"int {name} = 0;";
			case ResultType.Float:
				return $"float {name} = 0.0f;";
			case ResultType.Vector2:
				return $"float2 {name} = float2( 0.0f, 0.0f );";
			case ResultType.Vector3:
				return $"float3 {name} = float3( 0.0f, 0.0f, 0.0f );";
			case ResultType.Vector4:
				return $"float4 {name} = float4( 0.0f, 0.0f, 0.0f, 0.0f );";
			case ResultType.Float2x2:
				return $"float2x2 {name} = float2x2( 0.0f, 0.0f, 0.0f, 0.0f );";
			case ResultType.Float3x3:
				return $"float3x3 {name} = float3x3( 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f );";
			case ResultType.Float4x4:
				return $"float4x4 {name} = float4x4( 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f );";
			default:
				throw new NotImplementedException( $"Unknown ResultType \"{resultType}\"" );
		}
	}

	private static ResultType GetResultTypeFromHLSLDataType( string DataType )
	{
		return DataType switch
		{
			"bool" => ResultType.Bool,
			"int" => ResultType.Int,
			"float" => ResultType.Float,
			"float2" => ResultType.Vector2,
			"float3" => ResultType.Vector3,
			"float4" => ResultType.Vector4,
			"float2x2" => ResultType.Float2x2,
			"float3x3" => ResultType.Float3x3,
			"float4x4" => ResultType.Float4x4,
			"Texture2D" => ResultType.Texture2D,
			"TextureCube" => ResultType.TextureCube,
			"SamplerState" => ResultType.Sampler,
			_ => throw new ArgumentException( $"Unknown DataType `{DataType}`" )
		};
	}

	private static IEnumerable<PropertyInfo> GetNodeInputProperties( Type type )
	{
		return type.GetProperties( BindingFlags.Instance | BindingFlags.Public )
			.Where( property => property.GetSetMethod() != null &&
			property.PropertyType == typeof( NodeInput ) &&
			property.IsDefined( typeof( BaseNodePlus.InputAttribute ), false ) );
	}

	private static string GenerateFunctions( CompileResult result )
	{
		if ( !result.Functions.Any() )
			return null;

		var sb = new StringBuilder();
		foreach ( var function in result.Functions )
		{
			if ( GraphHLSLFunctions.TryGetFunction( function, out var code ) )
			{
				sb.AppendLine();
				sb.Append( code );
			}
		}

		return sb.ToString();
	}
}