Files
2026-11-01 23:39:55 +02:00

348 lines
14 KiB
C#

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<IShaderFunction> 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<IShaderFunction> m_provider;
internal FunctionHeader Header { get; private set; }
internal Dictionary<string, ParameterHeader> ParamHeaders { get; private set; }
internal IProvider<IShaderFunction> 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<IShaderFunction> provider)
{
this.m_provider = (IProvider<IShaderFunction>)provider.Clone();
UpdateModel();
}
public override void UpdateNodeAfterDeserialization()
{
base.UpdateNodeAfterDeserialization();
Refresh();
}
public override void Concretize()
{
UpdateModel();
base.Concretize();
}
public bool Reload(HashSet<string> 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<MaterialSlot> previousSlots = new();
GetSlots(previousSlots);
Dictionary<string, (int inputId, int outputId)> oldSlotMap = new();
HashSet<int> 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<ParameterHeader> paramOrder = new();
List<int> 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<MaterialSlot>.Get(out var slots))
{
GetSlots<MaterialSlot>(slots);
Dictionary<string, (MaterialSlot input, MaterialSlot output)> 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));
}
}
}
}