Files
2026-07-13 13:13:17 +08:00

985 lines
38 KiB
C#

#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
{
/// <summary>
/// Represents an instance of a <see cref="Symbol"/> within a Symbol.
/// </summary>
public sealed class Child
{
/// <summary>A reference to the <see cref="Symbol"/> this is an instance from.</summary>
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<Guid, Input> Inputs { get; private init; } = new();
public Dictionary<Guid, Output> Outputs { get; private init; } = new();
internal IEnumerable<Instance> Instances
{
get
{
lock(_creationLock)
return _instancesOfSelf.Values;
}
}
private readonly Dictionary<int, Instance> _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;
/// <summary>The input value used for this symbol child</summary>
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<byte>(&symbolId, 16);
hashComputer.AppendData(symbolIdBytes);
if (parent != null)
{
var parentId = parent.Id;
var parentIdBytes = new ReadOnlySpan<byte>(&parentId, 16);
hashComputer.AppendData(parentIdBytes);
}
if (extra != null)
{
var val = extra.Value;
var bytes = new ReadOnlySpan<byte>(&val, 16);
hashComputer.AppendData(bytes);
}
// SHA1 is 20 bytes long, but we only need 16 bytes for a guid
var newGuidBytes = new ReadOnlySpan<byte>(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<Instance> 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<object>(),
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<Guid> 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<Guid> readOnlyList, [NotNullWhen(true)] out Child? child)
{
var parentSymbolChildId = readOnlyList[^2];
return Parent.ChildrenCreatedFromMe.TryGetValue(parentSymbolChildId, out child);
}
bool TryGetParentInstance(IReadOnlyList<Guid> 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<Guid> 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<Guid> 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<Guid>? path)
{
return SearchForChild(search, ReadOnlySpan<Guid>.Empty, out child, out path);
}
private bool SearchForChild(Guid search, ReadOnlySpan<Guid> path, out Child? child, [NotNullWhen(true)] out IReadOnlyList<Guid>? fullPath)
{
Span<Guid> 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();
}
}
}
/// <summary>
/// Disposes all live instances of this child (releasing native/GPU resources).
/// </summary>
/// <remarks>
/// Use only when the child is being removed or the parent is reloaded. This is immediate and irreversible; prefer reconnecting to preserve instance state.
/// </remarks>
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);
}
}
}
}
}