#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Security.Cryptography; using T3.Core.Compilation; using T3.Core.DataTypes; using T3.Core.Logging; using T3.Core.Model; using T3.Core.Operator.Slots; using T3.Core.Utils; using Texture2D = T3.Core.DataTypes.Texture2D; namespace T3.Core.Operator; public partial class Symbol { /// /// Represents an instance of a within a Symbol. /// public sealed class Child { /// A reference to the this is an instance from. public Symbol Symbol { get; } public Guid Id { get; } public Symbol? Parent { get; } public string Name { get; set; } public string ReadableName => string.IsNullOrEmpty(Name) ? Symbol.Name : Name; public bool HasCustomName => !string.IsNullOrEmpty(Name); public bool IsBypassed { get => _isBypassed; set => SetBypassed(value); } public bool IsDisabled { get { // Avoid LINQ because of allocations in inner loop foreach (var x in Outputs.Values) { if (x.IsDisabled) return true; } return false; //return Outputs.FirstOrDefault().Value?.IsDisabled ?? false; } set => SetDisabled(value); } public Dictionary Inputs { get; private init; } = new(); public Dictionary Outputs { get; private init; } = new(); internal IEnumerable Instances { get { lock(_creationLock) return _instancesOfSelf.Values; } } private readonly Dictionary _instancesOfSelf = []; private readonly object _creationLock; // ReSharper disable once NotAccessedField.Local private readonly bool _isGeneric; public Guid? PreviousId { get; private set; } internal Child(Symbol symbol, Guid childId, Symbol? parent, string? name, bool isBypassed, object creationLock, Guid? previousId = null) { _creationLock = creationLock; Symbol = symbol; Id = childId; Parent = parent; Name = name ?? string.Empty; _isBypassed = isBypassed; _isGeneric = symbol.IsGeneric; PreviousId = previousId; foreach (var inputDefinition in symbol.InputDefinitions) { if (!Inputs.TryAdd(inputDefinition.Id, new Input(inputDefinition))) { throw new ApplicationException($"The ID for symbol input {symbol.Name}.{inputDefinition.Name} must be unique."); } } foreach (var outputDefinition in symbol.OutputDefinitions) { Symbol.OutputDefinition.TryGetNewOutputDataType(outputDefinition, out var outputData); var output = new Output(outputDefinition, outputData) { DirtyFlagTrigger = outputDefinition.DirtyFlagTrigger }; if (!Outputs.TryAdd(outputDefinition.Id, output)) { throw new ApplicationException($"The ID for symbol output {symbol.Name}.{outputDefinition.Name} must be unique."); } } } private void SetDisabled(bool shouldBeDisabled) { if (Parent == null) return; var outputDefinitions = Symbol.OutputDefinitions; // Set disabled status on this child's outputs foreach (var outputDef in outputDefinitions) { if (outputDef == null) { Log.Warning($"{Symbol.GetType()} {Symbol.Name} contains a null {typeof(Symbol.OutputDefinition)}", Id); continue; } if (Outputs.TryGetValue(outputDef.Id, out var childOutput)) { childOutput.IsDisabled = shouldBeDisabled; } else { Log.Warning($"{typeof(Symbol.Child)} {ReadableName} does not have the following child output as defined: " + $"{childOutput?.OutputDefinition.Name}({nameof(Guid)}{childOutput?.OutputDefinition.Id})"); } } // Set disabled status on outputs of each instanced copy of this child within all parents that contain it foreach (var parentInstance in Parent.InstancesOfSelf) { // This parent doesn't have an instance of our SymbolChild. Ignoring and continuing. if (!parentInstance.Children.TryGetChildInstance(Id, out var matchingChildInstance)) continue; // Set disabled status on all outputs of each instance foreach (var slot in matchingChildInstance.Outputs) { slot.IsDisabled = shouldBeDisabled; } } } #region sub classes ============================================================= public sealed class Output { public Symbol.OutputDefinition OutputDefinition { get; } public IOutputData OutputData { get; } public bool IsDisabled { get; set; } public DirtyFlagTrigger DirtyFlagTrigger { get => _dirtyFlagTrigger ?? OutputDefinition.DirtyFlagTrigger; set => _dirtyFlagTrigger = (value != OutputDefinition.DirtyFlagTrigger) ? (DirtyFlagTrigger?)value : null; } private DirtyFlagTrigger? _dirtyFlagTrigger = null; internal Output(Symbol.OutputDefinition outputDefinition, IOutputData outputData) { OutputDefinition = outputDefinition; OutputData = outputData; } public Output DeepCopy() { return new Output(OutputDefinition, OutputData); } } public sealed class Input { public InputDefinition InputDefinition { get; } public Guid Id => InputDefinition.Id; public bool IsMultiInput => InputDefinition.IsMultiInput; public InputValue DefaultValue => InputDefinition.DefaultValue; public string Name => InputDefinition.Name; /// The input value used for this symbol child public InputValue Value { get; } public bool IsDefault { get; set; } public Input(Symbol.InputDefinition inputDefinition) { InputDefinition = inputDefinition; Value = DefaultValue.Clone(); IsDefault = true; } public void SetCurrentValueAsDefault() { if (DefaultValue.IsEditableInputReferenceType) { DefaultValue.AssignClone(Value); } else { DefaultValue.Assign(Value); } IsDefault = true; } public void ResetToDefault() { if (DefaultValue.IsEditableInputReferenceType) { Value.AssignClone(DefaultValue); } else { Value.Assign(DefaultValue); } IsDefault = true; } } #endregion #region Bypass private bool _isBypassed; public bool IsBypassable() { if (Symbol.OutputDefinitions.Count == 0) return false; if (Symbol.InputDefinitions.Count == 0) return false; var mainInput = Symbol.InputDefinitions[0]; var mainOutput = Symbol.OutputDefinitions[0]; var defaultValueType = mainInput.DefaultValue.ValueType; if (defaultValueType != mainOutput.ValueType) return false; return _bypassableTypes.Contains(defaultValueType); } private static readonly Type[] _bypassableTypes = { typeof(Command), typeof(Texture2D), typeof(BufferWithViews), typeof(MeshBuffers), typeof(float), typeof(Vector2), typeof(Vector3), typeof(string), typeof(ShaderGraphNode) }; private void SetBypassed(bool shouldBypass) { if (shouldBypass == _isBypassed) return; if (!IsBypassable()) return; if (Parent == null) { // Clarify: shouldn't this be shouldBypass? _isBypassed = shouldBypass; // during loading parents are not yet assigned. This flag will later be used when creating instances return; } lock (_creationLock) { if (_instancesOfSelf.Count == 0) { _isBypassed = shouldBypass; // while duplicating / cloning as new symbol there are no instances yet. return; } } // check if there is a connection var isOutputConnected = false; var mainOutputDef = Symbol.OutputDefinitions[0]; foreach (var connection in Parent.Connections) { if (connection.SourceSlotId != mainOutputDef.Id || connection.SourceParentOrChildId != Id) continue; isOutputConnected = true; break; } if (!isOutputConnected) return; var id = Id; foreach (var parentInstance in Parent.InstancesOfSelf) { var instance = parentInstance.Children[id]; Instance.SetBypassFor(instance, shouldBypass); } _isBypassed = shouldBypass; } #endregion Bypass public override string ToString() { return Parent?.Name + ">" + ReadableName; } internal static unsafe Guid CreateIdDeterministically(Symbol symbol, Symbol? parent, Guid? extra = null) { //deterministically create a new guid from the symbol id using var hashComputer = IncrementalHash.CreateHash(HashAlgorithmName.SHA1); var symbolId = symbol.Id; var symbolIdBytes = new ReadOnlySpan(&symbolId, 16); hashComputer.AppendData(symbolIdBytes); if (parent != null) { var parentId = parent.Id; var parentIdBytes = new ReadOnlySpan(&parentId, 16); hashComputer.AppendData(parentIdBytes); } if (extra != null) { var val = extra.Value; var bytes = new ReadOnlySpan(&val, 16); hashComputer.AppendData(bytes); } // SHA1 is 20 bytes long, but we only need 16 bytes for a guid var newGuidBytes = new ReadOnlySpan(hashComputer.GetHashAndReset(), 0, 16); return new Guid(newGuidBytes); } internal void RemoveChildInstancesOf(Child child) { var idToDestroy = child.Id; lock (_creationLock) { foreach (var instance in _instancesOfSelf.Values) { //var instance = instanceKvp.Value; if (instance.Children.TryGetChildInstance(idToDestroy, out var childInstance, false)) { childInstance.DisposePackage(null); } } } } private void DestroyAndClearAllInstances(SymbolPackage? onlyDisposeInPackage) { lock (_creationLock) { var allInstances = _instancesOfSelf.Values.ToArray(); for (int i = allInstances.Length - 1; i >= 0; i--) { allInstances[i].DisposePackage(onlyDisposeInPackage); // removes self from _instancesOfSelf dict } Debug.Assert(_instancesOfSelf.Count == 0, $"All instances of {Symbol.Name} should have been disposed, but {_instancesOfSelf.Count} remain."); } } internal void Dispose() { DestroyAndClearAllInstances(null); lock (_creationLock) { var removed = Symbol._childrenCreatedFromMe.Remove(Id, out _); Debug.Assert(removed); } } internal void AddChildInstances(Child newChild, ICollection listToAddNewInstancesTo) { lock (_creationLock) { foreach (var instance in _instancesOfSelf.Values) { var path = instance.InstancePath.Append(newChild.Id).ToArray(); if (newChild.TryGetOrCreateInstance(path, out var newInstance, out var created, true)) { if (created) { listToAddNewInstancesTo.Add(newInstance); } } } } } internal bool UpdateIOAndConnections(SlotChangeInfo slotChanges) { UpdateSymbolChildIO(this, slotChanges); if (Parent == null) { DestroyAndClearAllInstances(Symbol.SymbolPackage); // just destroy all instances - we have no connections to worry about since we dont have a parent return false; } // we dont need to update our instances/connections - our parents do that for us if they need it if (Parent.NeedsTypeUpdate && Parent.SymbolPackage == Symbol.SymbolPackage) { // destroy all instances if necessary? probably not... //DestroyAndClearAllInstances(); return false; } // deal with removed connections var parentConnections = Parent!.Connections; // get all connections that belong to this instance var connectionsToReplace = parentConnections.FindAll(c => c.SourceParentOrChildId == Id || c.TargetParentOrChildId == Id); // first remove those connections where the inputs/outputs doesn't exist anymore var connectionsToRemove = connectionsToReplace.FindAll(c => { return slotChanges.RemovedOutputDefinitions.Any(output => { var outputId = output.Id; return outputId == c.SourceSlotId || outputId == c.TargetSlotId; }) || slotChanges.RemovedInputDefinitions.Any(input => { var inputId = input.Id; return inputId == c.SourceSlotId || inputId == c.TargetSlotId; }); }); foreach (var connection in connectionsToRemove) { Parent.RemoveConnection(connection); // TODO: clarify if we need to iterate over all multi input indices connectionsToReplace.Remove(connection); } // now create the entries for those that will be reconnected after the instance has been replaced. Take care of the multi input order //connectionsToReplace.Reverse(); foreach (var con in connectionsToReplace) { Parent.ReplaceConnection(con); } return false; } private bool TryCreateNewInstance(Instance? parentInstance, [NotNullWhen(true)] out Instance? newInstance) { var path = parentInstance == null ? [Id] : parentInstance.InstancePath.Append(Id).ToArray(); var parent = parentInstance?.SymbolChild; var pathHash = HashCodeOf(path); lock (_creationLock) { if (_instancesOfSelf.TryGetValue(pathHash, out newInstance)) { // instance already exists //return true; throw new InvalidOperationException($"Instance {Name} with id ({Id}) already exists at path {string.Join(" > ", path)}"); } if (!TryCreateAndConnectInstance(parent, parentInstance, path, out newInstance, out var reason)) { Log.Error(reason); return false; } } return true; bool TryCreateAndConnectInstance(Symbol.Child? parentSymbolChild, Instance? parentInst, Guid[] newInstancePath, [NotNullWhen(true)] out Instance? newInstance, [NotNullWhen(false)] out string? reason2) { if(parentSymbolChild != null) { if(parentSymbolChild.Symbol != Parent) { throw new InvalidOperationException($"Parent symbol {parentSymbolChild.Symbol} does not match {Symbol}"); } if (newInstancePath[^2] != parentSymbolChild.Id) { throw new InvalidOperationException($"Instance path does not match parent id {parentSymbolChild.Id}"); } } else { if(Parent != null) throw new InvalidOperationException("symbol child has no parent but parent instance provided is not null"); } // make sure we're not instantiating a child that needs to be updated again later //Symbol.UpdateInstanceType(); if (!TryInstantiate(out newInstance, out reason2)) { Log.Error(reason2); return false; } if (!_instancesOfSelf.TryAdd(pathHash, newInstance)) { throw new InvalidOperationException("Attempted to create a new instance when one already exists at that path"); } newInstance.SetSymbolInfo(this, parentSymbolChild, newInstancePath, pathHash); newInstance.Initialize(parentInst); // cache property accesses for performance var newInstanceInputDefinitions = Symbol.InputDefinitions; var newInstanceInputDefinitionCount = newInstanceInputDefinitions.Count; var newInstanceInputs = newInstance.Inputs; var newInstanceInputCount = newInstanceInputs.Count; var symbolChildInputs = Inputs; // set up the inputs for the child instance for (int i = 0; i < newInstanceInputDefinitionCount; i++) { if (i >= newInstanceInputCount) { Log.Warning($"Skipping undefined input index"); continue; } var inputDefinitionId = newInstanceInputDefinitions[i].Id; var inputSlot = newInstanceInputs[i]; if (!symbolChildInputs.TryGetValue(inputDefinitionId, out var input)) { Log.Warning($"Skipping undefined input: {inputDefinitionId}"); continue; } inputSlot.Input = input; inputSlot.Id = inputDefinitionId; } // cache property accesses for performance var childOutputDefinitions = Symbol.OutputDefinitions; var childOutputDefinitionCount = childOutputDefinitions.Count; var childOutputs = newInstance.Outputs; var symbolChildOutputs = Outputs; // set up the outputs for the child instance for (int i = 0; i < childOutputDefinitionCount; i++) { Debug.Assert(i < childOutputs.Count); var outputDefinition = childOutputDefinitions[i]; var id = outputDefinition.Id; if (i >= childOutputs.Count) { Log.Warning($"Skipping undefined output: {id}"); continue; } var outputSlot = childOutputs[i]; outputSlot.Id = id; var symbolChildOutput = symbolChildOutputs[id]; if (outputDefinition.OutputDataType != null) { // output is using data, so link it if (outputSlot is IOutputDataUser outputDataConsumer) { outputDataConsumer.SetOutputData(symbolChildOutput.OutputData); } } outputSlot.DirtyFlag.Trigger = symbolChildOutput.DirtyFlagTrigger; outputSlot.IsDisabled = symbolChildOutput.IsDisabled; } return true; bool TryInstantiate([NotNullWhen(true)] out Instance? instance, [NotNullWhen(false)] out string? reason3) { if (Symbol.SymbolPackage.AssemblyInformation.OperatorTypeInfo.TryGetValue(Symbol.Id, out var typeInfo)) { var constructor = typeInfo.GetConstructor(); try { instance = (Instance)constructor.Invoke(); reason3 = string.Empty; return true; } catch (Exception e) { reason3 = $"Failed to create instance of type {Symbol.InstanceType} with id {Id}: {e}"; instance = null; return false; } } Log.Error($"No constructor found for {Symbol.InstanceType}. This should never happen!! Please report this"); try { // create instance through reflection instance = Activator.CreateInstance(Symbol.InstanceType, AssemblyInformation.ConstructorBindingFlags, binder: null, args: Array.Empty(), culture: null) as Instance; if (instance is null) { reason3 = $"(Instance creation fallback failure) Failed to create instance of type " + $"{Symbol.InstanceType} with id {Id} - result was null"; return false; } Log.Warning($"(Instance creation fallback) Created instance of type {Symbol.InstanceType} with id {Id} through reflection"); reason3 = string.Empty; return true; } catch (Exception e) { reason3 = $"(Instance creation fallback failure) Failed to create instance of type {Symbol.InstanceType} with id {Id}: {e}"; instance = null; return false; } } } } internal void AddConnectionToInstances(Connection connection, int multiInputIndex, bool allowCreate) { lock (_creationLock) { foreach (var instance in _instancesOfSelf.Values) { instance.TryAddConnection(connection, multiInputIndex, allowCreate); } } } internal void RemoveConnectionFromInstances(Connection connection, int multiInputIndex) { lock (_creationLock) { foreach (var instance in _instancesOfSelf.Values) { if (instance.TryGetTargetSlot(connection, out var targetSlot, false)) { targetSlot.RemoveConnection(multiInputIndex); // Ensure reconnect/replace operations always propagate dirtiness, // even when the slot still has other connections (e.g. multi-input replace). targetSlot.InvalidateGraph(); } } } } internal void InvalidateInputDefaultInInstances(in Guid inputId) { lock (_creationLock) { foreach (var instance in _instancesOfSelf.Values) { var inputSlots = instance.Inputs; for (int i = 0; i < inputSlots.Count; i++) { var slot = inputSlots[i]; if (slot.Id != inputId) continue; if (!slot.Input.IsDefault) continue; slot.DirtyFlag.Invalidate(); break; } } } } internal void InvalidateInputInChildren(in Guid inputId, in Guid childId) { lock (_creationLock) { foreach (var instanceInfo in _instancesOfSelf) { var instance = instanceInfo.Value; //var child = instance.Children[childId]; if (!instance.Children.TryGetChildInstance(childId, out var child)) { Log.Debug("Failed to invalidate missing child"); continue; } var inputSlots = child.Inputs; for (int j = 0; j < inputSlots.Count; j++) { var slot = inputSlots[j]; if (slot.Id != inputId) continue; slot.DirtyFlag.Invalidate(); break; } } } } internal void SortInputSlotsByDefinitionOrder() { lock (_creationLock) { foreach (var instance in _instancesOfSelf.Values) { Instance.SortInputSlotsByDefinitionOrder(instance); } } } internal void RemoveDisposedInstance(Instance child, int hash) { lock (_creationLock) { if (!_instancesOfSelf.Remove(hash)) { Log.Error($"Could not find instance {child} to remove from {this}"); } } } internal void PrepareForReload() { DestroyAndClearAllInstances(Symbol.SymbolPackage); } public bool TryGetOrCreateInstance(IReadOnlyList path, [NotNullWhen(true)] out Instance? instance, out bool created, bool allowCreate = true) { // throw exceptions if the path is invalid if (path.Count == 0) { throw new ArgumentException("Path must not be empty"); } if(!path[^1].Equals(Id)) { throw new ArgumentException($"Path must end with {Id}"); } if (Parent == null) { if(path.Count != 1) throw new ArgumentException("Path must be of length 1 if parent is null"); if(path[0] != Id) throw new ArgumentException($"Path must be {Id} if parent is null"); } else if (path.Count < 2) { throw new ArgumentException("Path must be of length 2 or more if parent is not null"); } var hash = HashCodeOf(path); lock (_creationLock) { if (_instancesOfSelf.TryGetValue(hash, out instance)) { // instance already exists created = false; if (allowCreate) { if (instance.TryGetParentInstance(out var parent, allowCreate) && parent is { NeedsInternalReconnections: true, Initialized: true }) { parent.ReconnectChildren(); } else if (instance.NeedsInternalReconnections && allowCreate) { instance.ReconnectChildren(); } } return true; } if(!allowCreate) { created = false; return false; } if (Parent == null) { created = TryCreateNewInstance(null, out instance); return created; } if (TryGetParentInstance(path, out created, out var parentInstance)) { // try to get our instance straight from the parent if (!parentInstance.Children.TryGetChildInstance(Id, out instance, false)) { // since we dont exist yet, lets create us created = TryCreateNewInstance(parentInstance, out instance); return created; } // if we're here, we already exist in the parent instance // we just need to make sure we add our instance to our own collection if (!_instancesOfSelf.ContainsKey(hash)) { // Synchronization issue detected: instance exists in parent but not in our tracking // This can happen during complex initialization or reload scenarios Log.Warning($"Instance {ReadableName} found in parent but missing from child tracking. Resyncing."); _instancesOfSelf[hash] = instance; } created = false; return true; } } created = false; return false; bool GetParentAsChild(IReadOnlyList readOnlyList, [NotNullWhen(true)] out Child? child) { var parentSymbolChildId = readOnlyList[^2]; return Parent.ChildrenCreatedFromMe.TryGetValue(parentSymbolChildId, out child); } bool TryGetParentInstance(IReadOnlyList guids, out bool wasCreated, [NotNullWhen(true)] out Instance? parentInstance) { if (!GetParentAsChild(guids, out var parentSymbolChild)) { parentInstance = null; wasCreated = false; return false; } //var parentSymbolChild = GetParentAsChild(guids); var parentPath = guids.SkipLast(1).ToArray(); var gotParent = parentSymbolChild.TryGetOrCreateInstance(parentPath, out parentInstance, out wasCreated); return gotParent; } // TODO: No longer needed? // void EnsureInstanceSlotsAreConnected(IReadOnlyList instancePath, Instance existingInstance) // { // if (existingInstance is { NeedsInternalReconnections: true }) // { // existingInstance.ReconnectChildren(); // } // // if (Parent is null) // { // // if we have no parent we have no connections to reconnect // return; // } // // if(TryGetParentInstance(instancePath, out var wasCreated, out var parent)) // { // if (wasCreated) // { // // if the parent was just created via this invocation, then this instance should have been marked as not needing reconnections // // so we can skip this // Log.Debug("Recreated parent instance from child reference", parent); // Debug.Assert(!existingInstance.NeedsInternalReconnections); // return; // } // // if (parent.NeedsInternalReconnections) // { // parent.ReconnectChildren(); // Log.Debug("Regenerated connections for parent", parent); // } // } // else // { // Log.Error("Instance needs reconnections but parent is null - this probably shouldn't happen", existingInstance); // } // } } public static int HashCodeOf(IReadOnlyList path) { int hash = path[0].GetHashCode(); for (int i = 1; i < path.Count; i++) { hash = HashCode.Combine(hash, path[i].GetHashCode()); } return hash; } public void ClearPreviousId() { PreviousId = null; } public bool SearchForChild(Guid search, [NotNullWhen(true)] out Child? child, [NotNullWhen(true)] out IReadOnlyList? path) { return SearchForChild(search, ReadOnlySpan.Empty, out child, out path); } private bool SearchForChild(Guid search, ReadOnlySpan path, out Child? child, [NotNullWhen(true)] out IReadOnlyList? fullPath) { Span pathIncludingMe = stackalloc Guid[path.Length + 1]; path.CopyTo(pathIncludingMe); pathIncludingMe[^1] = Id; if (Id == search) { child = this; fullPath = pathIncludingMe.ToArray(); return true; } var symbol = Symbol; foreach (var symbolChild in symbol.Children.Values) { if(symbolChild.SearchForChild(search, pathIncludingMe, out var foundChild, out var foundPath)) { child = foundChild; fullPath = foundPath; return true; } } child = null; fullPath = null; return false; } internal void ReconnectAllChildren() { lock (_creationLock) { foreach (var inst in _instancesOfSelf.Values) { inst.ReconnectChildren(); } } } /// /// Disposes all live instances of this child (releasing native/GPU resources). /// /// /// Use only when the child is being removed or the parent is reloaded. This is immediate and irreversible; prefer reconnecting to preserve instance state. /// public void DestroyAllInstances() { lock (_creationLock) { // toArray as a defensive copy - these instances will be removed from the dictionary as a result of calling this func foreach(var instance in _instancesOfSelf.Values.ToArray()) { instance.DisposePackage(null); } } } } }