#region using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using UnityEditor; using UnityEngine; using UnityEngine.Jobs; using UnityEngine.Profiling; #endregion namespace nadena.dev.modular_avatar.core.armature_lock { internal abstract class ArmatureLockOperator : IDisposable where T : ArmatureLockOperator, new() { internal static readonly T Instance = new T(); private static long LastHierarchyChange = 0; private TransformAccessArray _baseBones, _baseParentBones, _targetBones, _targetParentBones; private int _commitFilter; private bool _isDisposed = false; private JobHandle _lastJob; private List _jobs = new List(); private long LastCheckedHierarchy = -1; protected readonly NativeMemoryManager _memoryManager = new NativeMemoryManager(); private bool _transformAccessDirty = true; private Transform[] _baseTransforms = Array.Empty(), _targetTransforms = Array.Empty(); private Transform[] _baseParentTransforms = Array.Empty(), _targetParentTransforms = Array.Empty(); protected Transform[] BaseTransforms => _baseTransforms; protected Transform[] TargetTransforms => _targetTransforms; // Managed by _memoryManager private NativeArrayRef _in_baseBone, _in_targetBone, _out_baseBone, _out_targetBone; private NativeArrayRef _in_baseParentBone, _in_targetParentBone; private NativeArrayRef _out_dirty_baseBone, _out_dirty_targetBone; private NativeArrayRef _boneToJobIndex; // Not managed by _memoryManager (since they're not indexed by bone) private NativeArray _abortFlag, _didAnyWriteFlag; private ArmatureLockJobAccessor GetAccessor() { return new ArmatureLockJobAccessor() { _in_baseBone = _in_baseBone, _in_targetBone = _in_targetBone, _in_baseParentBone = _in_baseParentBone, _in_targetParentBone = _in_targetParentBone, _out_baseBone = _out_baseBone, _out_targetBone = _out_targetBone, _out_dirty_baseBone = _out_dirty_baseBone, _out_dirty_targetBone = _out_dirty_targetBone, _abortFlag = _abortFlag, _didAnyWriteFlag = _didAnyWriteFlag, _boneToJobIndex = _boneToJobIndex, _in_boneInUse = _memoryManager.InUseMask, }; } static ArmatureLockOperator() { Instance = new T(); #if UNITY_EDITOR EditorApplication.delayCall += StaticInit; #endif } protected ArmatureLockOperator() { #if UNITY_EDITOR AssemblyReloadEvents.beforeAssemblyReload += () => DeferDestroy.DestroyImmediate(this); #endif _memoryManager.OnSegmentMove += MoveTransforms; _in_baseBone = _memoryManager.CreateArray(); _in_targetBone = _memoryManager.CreateArray(); _out_baseBone = _memoryManager.CreateArray(); _out_targetBone = _memoryManager.CreateArray(); _in_baseParentBone = _memoryManager.CreateArray(); _in_targetParentBone = _memoryManager.CreateArray(); _out_dirty_baseBone = _memoryManager.CreateArray(); _out_dirty_targetBone = _memoryManager.CreateArray(); _boneToJobIndex = _memoryManager.CreateArray(); } protected abstract bool WritesBaseBones { get; } public void Dispose() { if (_isDisposed) return; _isDisposed = true; _lastJob.Complete(); if (_baseBones.isCreated) DeferDestroy.DeferDestroyObj(_baseBones); if (_targetBones.isCreated) DeferDestroy.DeferDestroyObj(_targetBones); if (_baseParentBones.isCreated) DeferDestroy.DeferDestroyObj(_baseParentBones); if (_targetParentBones.isCreated) DeferDestroy.DeferDestroyObj(_targetParentBones); DerivedDispose(); _memoryManager.Dispose(); if (_abortFlag.IsCreated) _abortFlag.Dispose(); if (_didAnyWriteFlag.IsCreated) _didAnyWriteFlag.Dispose(); } #if UNITY_EDITOR protected static void StaticInit() { EditorApplication.hierarchyChanged += () => { LastHierarchyChange += 1; }; UpdateLoopController.UpdateCallbacks += Instance.Update; // TODO: On global enable, reset all jobs to init state? //ArmatureLockConfig.instance.OnGlobalEnableChange += Instance.Invalidate; EditorApplication.playModeStateChanged += (change) => { // If we allow ourselves to simply enter play mode without a final update, any movement applied by // automatically leaving animation preview mode won't be applied, leaving any outfits in the wrong pose. if (change == PlayModeStateChange.ExitingEditMode) { Instance.Update(); } }; } #endif /// /// (Re-)initialize state for a single job /// /// protected abstract bool SetupJob(ISegment segment); /// /// Computes the new positions and status words for a given range of bones. /// /// /// /// /// protected abstract JobHandle Compute(ArmatureLockJobAccessor accessor, int? jobIndex, JobHandle dependency); public ArmatureLockJob RegisterLock(IEnumerable<(Transform, Transform)> transforms) { if (_isDisposed) throw new ObjectDisposedException("ArmatureLockOperator"); var immutableTransforms = transforms.ToImmutableList(); var segment = _memoryManager.Allocate(immutableTransforms.Count()); ArmatureLockJob job = null; job = new ArmatureLockJob( segment, immutableTransforms, () => RemoveJob(job), () => UpdateSingle(job) ); EnsureTransformCapacity(_memoryManager.AllocatedLength); for (int i = 0; i < job.Transforms.Count(); i++) { var (baseBone, mergeBone) = job.Transforms[i]; _baseTransforms[i + segment.Offset] = baseBone; _baseParentTransforms[i + segment.Offset] = baseBone.parent; _targetTransforms[i + segment.Offset] = mergeBone; _targetParentTransforms[i + segment.Offset] = mergeBone.parent; } int jobIndex = _jobs.IndexOf(null); if (jobIndex >= 0) { _jobs[jobIndex] = job; } else { jobIndex = _jobs.Count(); _jobs.Add(job); } EnsureJobFlagCapacity(); for (int i = 0; i < segment.Length; i++) { _boneToJobIndex.Array[segment.Offset + i] = jobIndex; } _transformAccessDirty = true; bool ok = false; try { ok = SetupJob(segment); } finally { if (!ok) { // Initial setup failed; roll things back job.IsValid = false; job.Dispose(); } } return job; } private void RemoveJob(ArmatureLockJob job) { int index = _jobs.IndexOf(job); if (index < 0) return; _jobs[index] = null; job.Segment.Dispose(); } private void EnsureJobFlagCapacity() { if (_abortFlag.IsCreated && _abortFlag.Length >= _jobs.Count) return; var priorLength = _abortFlag.Length; if (_abortFlag.IsCreated) _abortFlag.Dispose(); if (_didAnyWriteFlag.IsCreated) _didAnyWriteFlag.Dispose(); int targetSize = Math.Max(Math.Max(16, _jobs.Count), (int)(priorLength * 1.5f)); _abortFlag = new NativeArray(targetSize, Allocator.Persistent); _didAnyWriteFlag = new NativeArray(targetSize, Allocator.Persistent); } private void EnsureTransformCapacity(int targetLength) { if (targetLength == _baseTransforms.Length) return; Array.Resize(ref _baseTransforms, targetLength); Array.Resize(ref _baseParentTransforms, targetLength); Array.Resize(ref _targetTransforms, targetLength); Array.Resize(ref _targetParentTransforms, targetLength); } private void MoveTransforms(int oldoffset, int newoffset, int length) { Array.Copy(_baseTransforms, oldoffset, _baseTransforms, newoffset, length); Array.Copy(_baseParentTransforms, oldoffset, _baseParentTransforms, newoffset, length); Array.Copy(_targetTransforms, oldoffset, _targetTransforms, newoffset, length); Array.Copy(_targetParentTransforms, oldoffset, _targetParentTransforms, newoffset, length); _transformAccessDirty = true; } public void Update() { InternalUpdate(); } private void UpdateSingle(ArmatureLockJob job) { var index = _jobs.IndexOf(job); if (index < 0) return; InternalUpdate(index); } private void InternalUpdate(int? jobIndex = null) { if (_isDisposed) return; SingleUpdate(jobIndex); } private long CycleStartHierarchyIndex = -1; private int _nextCheckIndex = 0; private void SingleUpdate(int? jobIndex) { if (_jobs.Count == 0) return; if (_isDisposed) return; Profiler.BeginSample("InternalUpdate"); _lastJob.Complete(); EnsureJobFlagCapacity(); if (_transformAccessDirty) { Profiler.BeginSample("RecreateTransformAccess"); if (_baseBones.isCreated && _baseBones.length == _baseTransforms.Length) { _baseBones.SetTransforms(_baseTransforms); _baseParentBones.SetTransforms(_baseParentTransforms); _targetBones.SetTransforms(_targetTransforms); _targetParentBones.SetTransforms(_targetParentTransforms); } else { if (_baseBones.isCreated) _baseBones.Dispose(); if (_targetBones.isCreated) _targetBones.Dispose(); if (_baseParentBones.isCreated) _baseParentBones.Dispose(); if (_targetParentBones.isCreated) _targetParentBones.Dispose(); _baseBones = new TransformAccessArray(_baseTransforms); _baseParentBones = new TransformAccessArray(_baseParentTransforms); _targetBones = new TransformAccessArray(_targetTransforms); _targetParentBones = new TransformAccessArray(_targetParentTransforms); } _transformAccessDirty = false; Profiler.EndSample(); } var accessor = GetAccessor(); for (int i = 0; i < _jobs.Count; i++) { accessor._abortFlag[i] = (_jobs[i] == null) || !_jobs[i].IsValid; accessor._didAnyWriteFlag[i] = false; } _lastJob = ReadTransforms(jobIndex); _lastJob = Compute(accessor, jobIndex, _lastJob); if (LastCheckedHierarchy != LastHierarchyChange) { Profiler.BeginSample("Recheck"); int startCheckIndex = _nextCheckIndex; do { if (_nextCheckIndex == 0) { CycleStartHierarchyIndex = LastHierarchyChange; } var job = _jobs[_nextCheckIndex % _jobs.Count]; _nextCheckIndex = (1 + _nextCheckIndex) % _jobs.Count; if (job != null && job.HierarchyChanged) { job.IsValid = false; } } while (_nextCheckIndex != startCheckIndex && !_lastJob.IsCompleted); if (_nextCheckIndex == 0) { LastCheckedHierarchy = CycleStartHierarchyIndex; } Profiler.EndSample(); } // Before committing, do a spot check of any bones that moved, to see if their parents changed. // This is needed because the hierarchyChanged event fires after Update ... _lastJob.Complete(); Profiler.BeginSample("Revalidate dirty bones"); int boneBase = 0; bool anyDirty = false; for (int job = 0; job < _jobs.Count; job++) { if (accessor._abortFlag[job]) continue; int curBoneBase = boneBase; boneBase += _jobs[job].Transforms.Count; if (!accessor._didAnyWriteFlag[job]) continue; for (int b = curBoneBase; b < boneBase; b++) { if (accessor._out_dirty_targetBone[b] || accessor._out_dirty_baseBone[b]) { anyDirty = true; if (_jobs[job].BoneChanged(b - curBoneBase)) { accessor._abortFlag[job] = true; _jobs[job].IsValid = false; break; } } } } Profiler.EndSample(); if (anyDirty) { _lastJob = CommitTransforms(jobIndex, _lastJob); _lastJob.Complete(); } for (int i = 0; i < _jobs.Count; i++) { if (_jobs[i] == null) continue; if (accessor._abortFlag[i]) { _jobs[i].IsValid = false; } else { _jobs[i].MarkLoop(); _jobs[i].WroteAny = accessor._didAnyWriteFlag[i]; } } Profiler.EndSample(); } protected virtual void DerivedDispose() { // default no-op } #region Job logic [BurstCompile] struct CopyTransformState : IJobParallelFor { [ReadOnly] public NativeArray _in; [WriteOnly] public NativeArray _out; public void Execute(int index) { _out[index] = _in[index]; } } [BurstCompile] struct ReadTransformsJob : IJobParallelForTransform { [WriteOnly] public NativeArray _bone; [ReadOnly] public NativeArray _boneToJobIndex; [ReadOnly] public NativeArray _boneInUse; [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [WriteOnly] public NativeArray _abortFlag; [BurstCompile] public void Execute(int index, TransformAccess transform) { if (!_boneInUse[index]) return; #if UNITY_2021_1_OR_NEWER if (!transform.isValid) { _abortFlag[_boneToJobIndex[index]] = true; return; } #endif _bone[index] = new TransformState { localPosition = transform.localPosition, localRotation = transform.localRotation, localScale = transform.localScale, localToWorldMatrix = transform.localToWorldMatrix, }; } } JobHandle ReadTransforms(int? jobIndex) { var accessor = GetAccessor(); var baseRead = new ReadTransformsJob() { _bone = accessor._in_baseBone, _boneToJobIndex = accessor._boneToJobIndex, _abortFlag = accessor._abortFlag, _boneInUse = accessor._in_boneInUse, }.ScheduleReadOnly(_baseBones, 32); baseRead = new CopyTransformState() { _in = accessor._in_baseBone, _out = accessor._out_baseBone }.Schedule(accessor._in_baseBone.Length, 32, baseRead); var targetRead = new ReadTransformsJob() { _bone = accessor._in_targetBone, _boneToJobIndex = accessor._boneToJobIndex, _abortFlag = accessor._abortFlag, _boneInUse = accessor._in_boneInUse, }.ScheduleReadOnly(_targetBones, 32); targetRead = new CopyTransformState() { _in = accessor._in_targetBone, _out = accessor._out_targetBone }.Schedule(accessor._in_targetBone.Length, 32, targetRead); var baseParentRead = new ReadTransformsJob() { _bone = accessor._in_baseParentBone, _boneToJobIndex = accessor._boneToJobIndex, _abortFlag = accessor._abortFlag, _boneInUse = accessor._in_boneInUse, }.ScheduleReadOnly(_baseParentBones, 32); var targetParentRead = new ReadTransformsJob() { _bone = accessor._in_targetParentBone, _boneToJobIndex = accessor._boneToJobIndex, _abortFlag = accessor._abortFlag, _boneInUse = accessor._in_boneInUse, }.ScheduleReadOnly(_targetParentBones, 32); return JobHandle.CombineDependencies( JobHandle.CombineDependencies(baseRead, targetRead), JobHandle.CombineDependencies(baseParentRead, targetParentRead) ); } [BurstCompile] struct CommitTransformsJob : IJobParallelForTransform { [ReadOnly] public NativeArray _boneState; [ReadOnly] public NativeArray _dirtyBoneFlag; [ReadOnly] public NativeArray _boneToJobIndex; [ReadOnly] public NativeArray _boneInUse; [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [ReadOnly] public NativeArray _abortFlag; public int jobIndexFilter; [BurstCompile] public void Execute(int index, TransformAccess transform) { #if UNITY_2021_1_OR_NEWER if (!transform.isValid) return; #endif if (!_boneInUse[index]) return; var jobIndex = _boneToJobIndex[index]; if (jobIndexFilter >= 0 && jobIndex != jobIndexFilter) return; if (_abortFlag[jobIndex]) return; if (!_dirtyBoneFlag[index]) return; transform.localPosition = _boneState[index].localPosition; transform.localRotation = _boneState[index].localRotation; transform.localScale = _boneState[index].localScale; } } JobHandle CommitTransforms(int? jobIndex, JobHandle prior) { var accessor = GetAccessor(); JobHandle job = new CommitTransformsJob() { _boneState = accessor._out_targetBone, _dirtyBoneFlag = accessor._out_dirty_targetBone, _boneToJobIndex = accessor._boneToJobIndex, _abortFlag = accessor._abortFlag, jobIndexFilter = jobIndex ?? -1, _boneInUse = accessor._in_boneInUse, }.Schedule(_targetBones, prior); if (WritesBaseBones) { var job2 = new CommitTransformsJob() { _boneState = accessor._out_baseBone, _dirtyBoneFlag = accessor._out_dirty_baseBone, _boneToJobIndex = accessor._boneToJobIndex, _abortFlag = accessor._abortFlag, jobIndexFilter = jobIndex ?? -1, _boneInUse = accessor._in_boneInUse, }.Schedule(_baseBones, prior); return JobHandle.CombineDependencies(job, job2); } else { return job; } } #endregion } }