using UnityEngine; using System.Collections.Generic; using UnityEditor.Graphing; using System; using Unity.GraphAuthoring.Editor.ProviderSystem; namespace UnityEditor.ShaderGraph.ProviderSystem { // A Base Node serving as the entry point for Provider based function definitions. // This Model object can work with any sort of function provider, interpret the function definition, // and generate a valid model representation of that object. This abstracts the need for node definitions // to be aware of how the model functions. // NOTE: There exists an ExpressionProvider currently, so it's not a problem to have Models that are specializations, // but ideally the expression input type would be gracefully handled in a generic way and it wouldn't be necessary. [Serializable] [HasDependencies(typeof(MinimalProviderNode))] internal class ProviderNode : AbstractMaterialNode, IHasAssetDependencies, IGeneratesBodyCode, IGeneratesFunction { [Serializable] internal class MinimalProviderNode : IHasDependencies { [SerializeReference] IProvider m_provider; public void GetSourceAssetDependencies(AssetCollection assetCollection) { if (m_provider == null || m_provider.AssetID == default) return; assetCollection.AddAssetDependency(m_provider.AssetID, AssetCollection.Flags.SourceDependency | AssetCollection.Flags.ArtifactDependency | AssetCollection.Flags.IncludeInExportPackage); } } // Reserves a control for the function selector. This is a bit of a hack. [GroupVariantControl] internal ProviderNode groupVariantSentinel => this; [SerializeReference] IProvider m_provider; internal FunctionHeader Header { get; private set; } internal Dictionary ParamHeaders { get; private set; } internal IProvider Provider => m_provider; const int kReservedOutputSlot = 0; public override bool hasPreview => true; public override bool hasUserAuthoredCode => true; public override bool canSetPrecision => Header?.allowPrecision ?? false; public override string documentationURL => Header?.helpUrl ?? base.documentationURL; internal override bool ExposeToSearcher => false; internal virtual bool requiresGeneration => false; public ProviderNode() { name = "Provider Based Node"; Header = new(); } internal void InitializeFromProvider(IProvider provider) { this.m_provider = (IProvider)provider.Clone(); UpdateModel(); } public override void UpdateNodeAfterDeserialization() { base.UpdateNodeAfterDeserialization(); Refresh(); } public override void Concretize() { UpdateModel(); base.Concretize(); } public bool Reload(HashSet changedAssetGuids) { // Scripted providers don't react to asset changes — their definition is in-memory. if (Provider.AssetID != default && changedAssetGuids.Contains(Provider.AssetID.ToString())) { Refresh(); return true; } return false; } protected void Refresh() { Provider?.Reload(); UpdateModel(); ValidateNode(); Dirty(ModificationScope.Graph); } internal void UpdateModel() { if (Provider == null || Provider.Definition == null || !Provider.IsValid) return; Header.Process(Provider.Definition, Provider); var header = Header; this.name = header.displayName; this.synonyms = header.searchTerms; var parameters = Provider.Definition.Parameters; List previousSlots = new(); GetSlots(previousSlots); Dictionary oldSlotMap = new(); HashSet usedSlotIds = new(); // build a map of parameter names to old slot ids (tuple is to support inout params). foreach (var oldSlot in previousSlots) { if (!oldSlotMap.TryGetValue(oldSlot.shaderOutputName, out var idTuple)) idTuple = (-1, -1); if (oldSlot.isInputSlot) idTuple.inputId = oldSlot.id; if (oldSlot.isOutputSlot) idTuple.outputId = oldSlot.id; oldSlotMap[oldSlot.shaderOutputName] = idTuple; } ParamHeaders = new(); List paramOrder = new(); List desiredSlotOrder = new(); // return type is a special case, because it has no parameter but still needs a slot // we reserve the 0 slotId for this and always make sure it's added first/at the top. if (header.hasReturnValueType) { usedSlotIds.Add(kReservedOutputSlot); desiredSlotOrder.Add(kReservedOutputSlot); AddSlotFromParameter(header.returnHeader, kReservedOutputSlot, SlotType.Output); } // build the header data for our parameters and mark which slot ids are being reused. foreach(var param in parameters) { var paramHeader = new ParameterHeader(param, Provider); ParamHeaders.TryAdd(param.Name, paramHeader); paramOrder.Add(paramHeader); if (oldSlotMap.TryGetValue(param.Name, out var idTuple)) { if (idTuple.inputId > -1 && param.IsInput) usedSlotIds.Add(idTuple.inputId); if (idTuple.outputId > -1 && param.IsOutput) usedSlotIds.Add(idTuple.outputId); } } // walk through our header data and build the actual slots. int nextSlot = kReservedOutputSlot; foreach (var paramHeader in paramOrder) { if (!oldSlotMap.TryGetValue(paramHeader.referenceName, out var idTuple)) idTuple = (-1, -1); void DoSlot(int slotId, SlotType dir) { if (slotId == -1) { while (usedSlotIds.Contains(++nextSlot)); usedSlotIds.Add(nextSlot); slotId = nextSlot; } AddSlotFromParameter(paramHeader, slotId, dir); desiredSlotOrder.Add(slotId); } if (paramHeader.isInput) DoSlot(idTuple.inputId, SlotType.Input); if (paramHeader.isOutput) DoSlot(idTuple.outputId, SlotType.Output); } RemoveSlotsNameNotMatching(usedSlotIds, true); SetSlotOrder(desiredSlotOrder); } static Rendering.ShaderCompilerMessageSeverity ToCompilerSeverity(MessageType severity) => severity switch { MessageType.Error => Rendering.ShaderCompilerMessageSeverity.Error, _ => Rendering.ShaderCompilerMessageSeverity.Warning, }; void AddSlotFromParameter(ParameterHeader header, int slotId, SlotType dir) { var slot = HeaderUtils.MakeSlotFromParameter(header, slotId, dir); if (slot != null) // Slots that use label control have no indication in the Model that they use them. // However, the only _existing_ case where they'd meaningfully change are SpaceMaterialSlots, // which also don't handle default values well either. In this case, we go nuclear and completely // rebuild the slot. AddSlot(slot, slot is not SpaceMaterialSlot, slot is not SpaceMaterialSlot); } public void GenerateNodeCode(ShaderStringBuilder sb, GenerationMode generationMode) { if (this.Provider == null || this.Provider.Definition == null) return; // Apply legacy precision/dynamic only before code generation/usage. HeaderUtils.TryApplyLegacy(Provider.Definition, this.Header, this.ParamHeaders, PrecisionUtil.Token, lastKnownDynamicVectorLength, out var func); using (UnityEngine.Pool.ListPool.Get(out var slots)) { GetSlots(slots); Dictionary paramSlotMap = new(); MaterialSlot returnSlot = null; // for various reasons, slots won't match one-to-one with parameters; // we'll need to make sure slots are matched to parameters in the order expected // while also accounting for inout duplicity. foreach (var slot in slots) { if (slot.id == kReservedOutputSlot) returnSlot = slot; else { paramSlotMap.TryGetValue(slot.shaderOutputName, out var tup); if (slot.isInputSlot) tup.input = slot; if (slot.isOutputSlot) tup.output = slot; paramSlotMap[slot.shaderOutputName] = tup; } } ShaderStringBuilder call = new(); // for the callsite. ShaderStringBuilder args = new(); // for building up the argument list. // The return result is an output slot, but will instead get initialized by the function call. if (returnSlot != null) { call.Append($"{HeaderUtils.ToShaderType(returnSlot)} {GetVariableNameForSlot(returnSlot.id)} = "); } bool first = true; foreach (var param in func.Parameters) { var inputSlot = paramSlotMap[param.Name].input; var outputSlot = paramSlotMap[param.Name].output; var typeString = outputSlot != null ? HeaderUtils.ToShaderType(outputSlot) : HeaderUtils.ToShaderType(inputSlot); var variableName = outputSlot != null ? GetVariableNameForSlot(outputSlot.id) : null; var valueString = inputSlot != null ? GetSlotValue(inputSlot.id, generationMode) : null; if (inputSlot != null && inputSlot.bareResource) // Texture/Sampler slots are normally 'Unity' wrapper structures valueString += inputSlot is SamplerStateMaterialSlot ? ".samplerstate" : ".tex"; var argument = valueString; // assume it's an input, in which case the arg will be the upstream value/connection. // apply linkage override if (ParamHeaders.TryGetValue(param.Name, out var ph) && ph.isLinkage) { var targetSlots = paramSlotMap[ph.linkTarget]; // this concept is awkward for inout slots, but that could be improved in the future. // ie. add support for int and set it to 0 = none, 1 = in, 2 = out, 3 = both. if (targetSlots.input != null && targetSlots.input.isConnected || targetSlots.output != null && targetSlots.output.isConnected) { argument = "true"; } else argument = "false"; } if (outputSlot != null) { // inout and out define a variable to be used as the argument. // but only inout gets initialized by the value. argument = variableName; sb.AddLine($"{typeString} {variableName}{(inputSlot != null ? $" = {valueString}" : "")};"); } if (!first) args.Append(", "); first = false; args.Append(argument); } call.Append(ShaderObjectUtils.GenerateCall(func, args.ToString())); sb.AddLine(call.ToString()); } } public void GenerateNodeFunction(FunctionRegistry registry, GenerationMode generationMode) { if (Provider == null || !Provider.IsValid || Provider.Definition == null) return; // Asset-backed providers contribute their source .hlsl as an include; scripted // providers carry their source inline via the generated function body below. if (Provider.AssetID != default) { var includePath = AssetDatabase.GUIDToAssetPath(Provider.AssetID); registry.RequiresIncludePath(includePath, false); } if (HeaderUtils.TryApplyLegacy(Provider.Definition, Header, ParamHeaders, PrecisionUtil.Token, lastKnownDynamicVectorLength, out var func) || requiresGeneration) { string code = ShaderObjectUtils.GenerateCode(func, false, false, true); registry.ProvideFunction(func.Name, s => s.AppendLine(code)); } } public override void ValidateNode() { base.ValidateNode(); if (Provider == null) { owner?.AddSetupError(this.objectId, "Node is in an invalid state and cannot recover."); return; } else if (Provider.AssetID != default && (Provider.Definition == null || !Provider.Definition.IsValid)) { var path = AssetDatabase.GUIDToAssetPath(Provider.AssetID); owner?.AddSetupError(this.objectId, $"Data for '{Provider.ProviderKey}' expected in '{path}'."); return; } else { foreach (var (msg, severity) in Header.Messages) owner?.AddValidationError(this.objectId, msg, ToCompilerSeverity(severity)); foreach (var param in ParamHeaders.Values) foreach (var (msg, severity) in param.Messages) owner?.AddValidationError(this.objectId, msg, ToCompilerSeverity(severity)); } } } }