diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental.meta b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental.meta new file mode 100644 index 000000000..191437141 --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6d54c2e139853683fa204952a4cbb4b0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFrame.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFrame.cs new file mode 100644 index 000000000..b2094adc9 --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFrame.cs @@ -0,0 +1,307 @@ +// Copyright (c) 2023 homuler +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +using System; +using System.Collections; +using System.Collections.Generic; +using Unity.Collections; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.Experimental.Rendering; +using UnityEngine.Rendering; + +namespace Mediapipe.Unity.Experimental +{ +#pragma warning disable IDE0065 + using Color = UnityEngine.Color; +#pragma warning restore IDE0065 + + public class TextureFrame : IDisposable + { + public class ReleaseEvent : UnityEvent { } + + private const string _TAG = nameof(TextureFrame); + + internal const int MaxTotalCount = 100; + + private static readonly GlobalInstanceTable _InstanceTable = new GlobalInstanceTable(MaxTotalCount); + /// + /// A dictionary to look up which native texture belongs to which . + /// + /// + /// Not all the instances are registered. + /// Texture names are queried only when necessary, and the corresponding data will be saved then. + /// + private static readonly Dictionary _NameTable = new Dictionary(); + + private readonly Texture2D _texture; + public Texture texture => _texture; + + private IntPtr _nativeTexturePtr = IntPtr.Zero; + private GlSyncPoint _glSyncToken; + + private readonly Guid _instanceId; + // NOTE: width and height can be accessed from a thread other than Main Thread. + public readonly int width; + public readonly int height; + public readonly TextureFormat format; + + public ImageFormat.Types.Format imageFormat => format.ToImageFormat(); + + public bool isReadable => _texture.isReadable; + + // TODO: determine at runtime + public GpuBufferFormat gpuBufferformat => GpuBufferFormat.kBGRA32; + + /// + /// The event that will be invoked when the TextureFrame is released. + /// +#pragma warning disable IDE1006 // UnityEvent is PascalCase + public readonly ReleaseEvent OnRelease; +#pragma warning restore IDE1006 + + private TextureFrame(Texture2D texture) + { + _texture = texture; + width = texture.width; + height = texture.height; + format = texture.format; + OnRelease = new ReleaseEvent(); + _instanceId = Guid.NewGuid(); + _InstanceTable.Add(_instanceId, this); + } + + public TextureFrame(int width, int height, TextureFormat format) : this(new Texture2D(width, height, format, false)) { } + public TextureFrame(int width, int height) : this(width, height, TextureFormat.RGBA32) { } + + public void Dispose() + { + RemoveAllReleaseListeners(); + if (_nativeTexturePtr != IntPtr.Zero) + { + var name = (uint)_nativeTexturePtr; + lock (((ICollection)_NameTable).SyncRoot) + { + var _ = _NameTable.Remove(name); + } + } + _glSyncToken?.Dispose(); + } + + public void CopyTexture(Texture dst) => Graphics.CopyTexture(_texture, dst); + + public void CopyTextureFrom(Texture src) => Graphics.CopyTexture(src, _texture); + + public bool ConvertTexture(Texture dst) => Graphics.ConvertTexture(_texture, dst); + + public bool ConvertTextureFrom(Texture src) => Graphics.ConvertTexture(src, _texture); + + /// + /// Copy texture data from . + /// If format is different from , it converts the format. + /// + /// + /// After calling it, pixel data can't be read from CPU safely. + /// + public bool ReadTextureOnGPU(Texture src) + { + if (GetTextureFormat(src) != format) + { + return Graphics.ConvertTexture(src, _texture); + } + Graphics.CopyTexture(src, _texture); + return true; + } + + public AsyncGPUReadbackRequest ReadTextureAsync(Texture src) + { + if (!ReadTextureOnGPU(src)) + { + throw new InvalidOperationException("Failed to read texture on GPU"); + } + + return AsyncGPUReadback.Request(_texture, 0); + } + + public AsyncGPUReadbackRequest ReadTextureAsync(Texture src, bool flipVertically, bool flipHorizontally) + { + var tmpRenderTexture = RenderTexture.GetTemporary(src.width, src.height, 32); + var currentRenderTexture = RenderTexture.active; + RenderTexture.active = tmpRenderTexture; + + var scale = new Vector2(1.0f, 1.0f); + var offset = new Vector2(0.0f, 0.0f); + if (flipVertically) + { + scale.y = -1.0f; + offset.y = 1.0f; + } + if (flipHorizontally) + { + scale.x = -1.0f; + offset.x = 1.0f; + } + Graphics.Blit(src, tmpRenderTexture, scale, offset); + + RenderTexture.active = currentRenderTexture; + + return AsyncGPUReadback.Request(tmpRenderTexture, 0, (req) => + { + _texture.LoadRawTextureData(req.GetData()); + _texture.Apply(); + _ = RevokeNativeTexturePtr(); + RenderTexture.ReleaseTemporary(tmpRenderTexture); + }); + } + + public Color GetPixel(int x, int y) => _texture.GetPixel(x, y); + + public Color32[] GetPixels32() => _texture.GetPixels32(); + + public void SetPixels32(Color32[] pixels) + { + _texture.SetPixels32(pixels); + _texture.Apply(); + + if (!RevokeNativeTexturePtr()) + { + // If this line was executed, there must be a bug. + Logger.LogError("Failed to revoke the native texture."); + } + } + + public NativeArray GetRawTextureData() where T : struct => _texture.GetRawTextureData(); + + /// The texture's native pointer + public IntPtr GetNativeTexturePtr() + { + if (_nativeTexturePtr == IntPtr.Zero) + { + _nativeTexturePtr = _texture.GetNativeTexturePtr(); + var name = (uint)_nativeTexturePtr; + + lock (((ICollection)_NameTable).SyncRoot) + { + if (!AcquireName(name, _instanceId)) + { + throw new InvalidProgramException($"Another instance (id={_instanceId}) is using the specified name ({name}) now"); + } + _NameTable.Add(name, _instanceId); + } + } + return _nativeTexturePtr; + } + + public uint GetTextureName() => (uint)GetNativeTexturePtr(); + + public Guid GetInstanceID() => _instanceId; + + public ImageFrame BuildImageFrame() => new ImageFrame(imageFormat, width, height, 4 * width, GetRawTextureData()); + + public GpuBuffer BuildGpuBuffer(GlContext glContext) + { +#if UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX || UNITY_ANDROID + var glTextureBuffer = new GlTextureBuffer(GetTextureName(), width, height, gpuBufferformat, OnReleaseTextureFrame, glContext); + return new GpuBuffer(glTextureBuffer); +#else + throw new NotSupportedException("This method is only supported on Linux or Android"); +#endif + } + + public void RemoveAllReleaseListeners() => OnRelease.RemoveAllListeners(); + + // TODO: stop invoking OnRelease when it's already released + public void Release(GlSyncPoint token = null) + { + _glSyncToken?.Dispose(); + _glSyncToken = token; + OnRelease.Invoke(this); + } + + /// + /// Waits until the GPU has executed all commands up to the sync point. + /// This blocks the CPU, and ensures the commands are complete from the point of view of all threads and contexts. + /// + public void WaitUntilReleased() + { + if (_glSyncToken == null) + { + return; + } + _glSyncToken.Wait(); + _glSyncToken.Dispose(); + _glSyncToken = null; + } + + [AOT.MonoPInvokeCallback(typeof(GlTextureBuffer.DeletionCallback))] + public static void OnReleaseTextureFrame(uint textureName, IntPtr syncTokenPtr) + { + var isIdFound = _NameTable.TryGetValue(textureName, out var _instanceId); + + if (!isIdFound) + { + Logger.LogError(_TAG, $"nameof (name={textureName}) is released, but the owner TextureFrame is not found"); + return; + } + + var isTextureFrameFound = _InstanceTable.TryGetValue(_instanceId, out var textureFrame); + + if (!isTextureFrameFound) + { + Logger.LogWarning(_TAG, $"nameof owner TextureFrame of the released texture (name={textureName}) is already garbage collected"); + return; + } + + var _glSyncToken = syncTokenPtr == IntPtr.Zero ? null : new GlSyncPoint(syncTokenPtr); + textureFrame.Release(_glSyncToken); + } + + /// + /// Remove from if it's stale. + /// If does not exist in , do nothing. + /// + /// + /// If the instance whose id is owns now, it still removes . + /// + /// Return if name is available + private static bool AcquireName(uint name, Guid ownerId) + { + if (_NameTable.TryGetValue(name, out var id)) + { + if (ownerId != id && _InstanceTable.TryGetValue(id, out var _)) + { + // if instance is found, the instance is using the name. + Logger.LogVerbose($"{id} is using {name} now"); + return false; + } + var _ = _NameTable.Remove(name); + } + return true; + } + + private static TextureFormat GetTextureFormat(Texture texture) => GraphicsFormatUtility.GetTextureFormat(texture.graphicsFormat); + + /// + /// Remove the texture name from and empty . + /// This method needs to be called when an operation is performed that may change the internal texture. + /// + private bool RevokeNativeTexturePtr() + { + if (_nativeTexturePtr == IntPtr.Zero) + { + return true; + } + + var currentName = GetTextureName(); + if (!_NameTable.Remove(currentName)) + { + return false; + } + _nativeTexturePtr = IntPtr.Zero; + return true; + } + } +} diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFrame.cs.meta b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFrame.cs.meta new file mode 100644 index 000000000..3966e408f --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFrame.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00a453775b320b55690d6b0d62f89468 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFramePool.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFramePool.cs new file mode 100644 index 000000000..197e4438d --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFramePool.cs @@ -0,0 +1,154 @@ +// Copyright (c) 2023 homuler +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +using System; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; + +namespace Mediapipe.Unity.Experimental +{ + public class TextureFramePool : IDisposable + { + private const string _TAG = nameof(TextureFramePool); + + public readonly int textureWidth; + public readonly int textureHeight; + public readonly TextureFormat textureFormat; + public int poolSize { get; private set; } + + /// + /// A lock for exclusive access to and . + /// + private readonly ReaderWriterLockSlim _textureFramesLock = new(); + private readonly Queue _availableTextureFrames; + /// + /// key: TextureFrame's instance ID + /// + private readonly Dictionary _textureFramesInUse; + + /// + /// The total number of texture frames in the pool. + /// + public int frameCount => _availableTextureFrames.Count + _textureFramesInUse.Count; + + public TextureFramePool(int textureWidth, int textureHeight, TextureFormat textureFormat = TextureFormat.RGBA32, int poolSize = 10) + { + this.textureWidth = textureWidth; + this.textureHeight = textureHeight; + this.textureFormat = textureFormat; + if (poolSize > TextureFrame.MaxTotalCount) + { + throw new ArgumentException($"poolSize must be <= {TextureFrame.MaxTotalCount}"); + } + this.poolSize = poolSize; + + _availableTextureFrames = new Queue(poolSize); + _textureFramesInUse = new Dictionary(poolSize); + } + + void IDisposable.Dispose() + { + _textureFramesLock.EnterWriteLock(); + try + { + _availableTextureFrames.Clear(); + foreach (var textureFrame in _textureFramesInUse.Values) + { + textureFrame.Dispose(); + } + _textureFramesInUse.Clear(); + poolSize = 0; + } + finally + { + _textureFramesLock.ExitWriteLock(); + } + } + + public bool TryGetTextureFrame(out TextureFrame outFrame) + { + TextureFrame nextFrame = null; + var result = false; + + _textureFramesLock.EnterUpgradeableReadLock(); + try + { + if (poolSize <= frameCount) + { + if (_availableTextureFrames.Count > 0) + { + _textureFramesLock.EnterWriteLock(); + try + { + nextFrame = _availableTextureFrames.Dequeue(); + _textureFramesInUse.Add(nextFrame.GetInstanceID(), nextFrame); + result = true; + } + finally + { + _textureFramesLock.ExitWriteLock(); + } + } + } + else + { + nextFrame = CreateNewTextureFrame(); + _textureFramesLock.EnterWriteLock(); + try + { + _textureFramesInUse.Add(nextFrame.GetInstanceID(), nextFrame); + result = true; + } + finally + { + _textureFramesLock.ExitWriteLock(); + } + } + } + finally + { + _textureFramesLock.ExitUpgradeableReadLock(); + } + + outFrame = nextFrame; + if (result) + { + nextFrame.WaitUntilReleased(); + } + return result; + } + + private void OnTextureFrameRelease(TextureFrame textureFrame) + { + _textureFramesLock.EnterWriteLock(); + try + { + if (!_textureFramesInUse.Remove(textureFrame.GetInstanceID())) + { + // won't be run + Logger.LogWarning(_TAG, "The released texture does not belong to the pool"); + return; + } + + // NOTE: poolSize won't be changed, so just enqueue the released texture here. + _availableTextureFrames.Enqueue(textureFrame); + } + finally + { + _textureFramesLock.ExitWriteLock(); + } + } + + private TextureFrame CreateNewTextureFrame() + { + var textureFrame = new TextureFrame(textureWidth, textureHeight, textureFormat); + textureFrame.OnRelease.AddListener(OnTextureFrameRelease); + + return textureFrame; + } + } +} diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFramePool.cs.meta b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFramePool.cs.meta new file mode 100644 index 000000000..460c333c3 --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFramePool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a0b1fb55ed988f82aedcd9e04c46232 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/GlobalInstanceTable.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/GlobalInstanceTable.cs deleted file mode 100644 index 1e62eec9d..000000000 --- a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/GlobalInstanceTable.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2021 homuler -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Mediapipe.Unity -{ - public class GlobalInstanceTable where TValue : class - { - private readonly Dictionary> _table; - private readonly int _maxSize; - - public GlobalInstanceTable(int maxSize) - { - _table = new Dictionary>(); - _maxSize = maxSize; - } - - public void Add(TKey key, TValue value) - { - if (_table.Count >= _maxSize) - { - ClearUnusedKeys(); - } - - lock (((ICollection)_table).SyncRoot) - { - if (_table.Count >= _maxSize) - { - throw new InvalidOperationException("The table is full"); - } - - if (_table.ContainsKey(key)) - { - if (_table[key].TryGetTarget(out var currentValue)) - { - throw new ArgumentException("An instance with the same key already exists"); - } - _table[key].SetTarget(value); - } - else - { - _table[key] = new WeakReference(value); - } - } - } - - public bool TryGetValue(TKey key, out TValue value) - { - lock (((ICollection)_table).SyncRoot) - { - if (_table.ContainsKey(key)) - { - return _table[key].TryGetTarget(out value); - } - } - value = default; - return false; - } - - public void Clear() - { - lock (((ICollection)_table).SyncRoot) - { - _table.Clear(); - } - } - - public bool ContainsKey(TKey key) - { - return _table.ContainsKey(key); - } - - private void ClearUnusedKeys() - { - lock (((ICollection)_table).SyncRoot) - { - var deadKeys = _table.Where(x => !x.Value.TryGetTarget(out var target)).Select(x => x.Key).ToArray(); - - foreach (var key in deadKeys) - { - var _ = _table.Remove(key); - } - } - } - } -} diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Util/GlobalInstanceTable.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Util/GlobalInstanceTable.cs new file mode 100644 index 000000000..2c1661a6c --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Util/GlobalInstanceTable.cs @@ -0,0 +1,135 @@ +// Copyright (c) 2023 homuler +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Mediapipe +{ + // TODO: make GlobalInstanceTable internal + public class GlobalInstanceTable where TValue : class + { + private readonly ReaderWriterLockSlim _tableLock = new(); + private readonly Dictionary> _table = new(); + + private int _maxSize; + /// + /// The maximum number of instances that can be stored in the table. + /// It can be safely changed to a value less than the current number of instances, + /// but will fail till the number of instances becomes less than or equal to the new value. + /// + public int maxSize + { + get => _maxSize; + set + { + if (value < 0) + { + throw new ArgumentException("maxSize must be greater than or equal to 0"); + } + _maxSize = value; + } + } + + public GlobalInstanceTable(int maxSize = 0) + { + this.maxSize = maxSize; + } + + public void Add(TKey key, TValue value) + { + _tableLock.EnterWriteLock(); + try + { + if (_table.Count >= maxSize) + { + ClearUnusedKeys(); + } + + if (_table.Count >= maxSize) + { + throw new InvalidOperationException("The table is full"); + } + + if (_table.ContainsKey(key)) + { + if (_table[key].TryGetTarget(out var _)) + { + throw new ArgumentException("An instance with the same key already exists"); + } + _table[key].SetTarget(value); + } + else + { + _table[key] = new WeakReference(value); + } + } + finally + { + _tableLock.ExitWriteLock(); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + _tableLock.EnterReadLock(); + try + { + if (_table.ContainsKey(key)) + { + return _table[key].TryGetTarget(out value); + } + value = default; + return false; + } + finally + { + _tableLock.ExitReadLock(); + } + } + + public void Clear() + { + _tableLock.EnterWriteLock(); + try + { + _table.Clear(); + } + finally + { + _tableLock.ExitWriteLock(); + } + } + + public bool ContainsKey(TKey key) + { + _tableLock.EnterReadLock(); + try + { + return _table.ContainsKey(key); + } + finally + { + _tableLock.ExitReadLock(); + } + } + + /// + /// Aquire the write lock before calling this method. + /// + private void ClearUnusedKeys() + { + foreach (var pair in _table) + { + if (!pair.Value.TryGetTarget(out var _)) + { + var _ = _table.Remove(pair.Key); + } + } + } + } +} diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/GlobalInstanceTable.cs.meta b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Util/GlobalInstanceTable.cs.meta similarity index 83% rename from Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/GlobalInstanceTable.cs.meta rename to Packages/com.github.homuler.mediapipe/Runtime/Scripts/Util/GlobalInstanceTable.cs.meta index cb9c81c08..27d98df72 100644 --- a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/GlobalInstanceTable.cs.meta +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Util/GlobalInstanceTable.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2732ac5c3629755e19f5eeeba29bfe79 +guid: 5ba0a990475916b059bd60c7a6408700 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Mediapipe.Tests.EditMode.asmdef b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Mediapipe.Tests.EditMode.asmdef index 457626856..9a34f8a0c 100644 --- a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Mediapipe.Tests.EditMode.asmdef +++ b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Mediapipe.Tests.EditMode.asmdef @@ -4,7 +4,8 @@ "references": [ "UnityEngine.TestRunner", "UnityEditor.TestRunner", - "Mediapipe.Runtime" + "Mediapipe.Runtime", + "Unity.PerformanceTesting" ], "includePlatforms": [ "Editor" diff --git a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/TextureFrameTest.cs b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/TextureFrameTest.cs new file mode 100644 index 000000000..c2b9c1fd9 --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/TextureFrameTest.cs @@ -0,0 +1,45 @@ +// Copyright (c) 2023 homuler +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +using System.Threading; +using Mediapipe.Unity.Experimental; +using NUnit.Framework; +using Unity.PerformanceTesting; +using UnityEngine; + +namespace Mediapipe.Tests +{ + public class TextureFramePoolTest + { + #region TryGetTextureFrame + [Test, Performance] + public void TryGetTextureFrame_Performance_When_5_Threads_Running() + { + var textureFramePool = new TextureFramePool(640, 480, TextureFormat.RGBA32, 20); + Measure.Method(() => + { + var i = 0; + while (i < 100) + { + if (!textureFramePool.TryGetTextureFrame(out var textureFrame)) + { + Thread.Sleep(1); + continue; + } + i++; + new Thread(() => + { + textureFrame.Release(); + }).Start(); + } + }).WarmupCount(1) + .MeasurementCount(20) + .IterationsPerMeasurement(100) + .Run(); + } + #endregion + } +} diff --git a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/TextureFrameTest.cs.meta b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/TextureFrameTest.cs.meta new file mode 100644 index 000000000..370159119 --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/TextureFrameTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ab12d7154f3ce220d8662aaf67d1bf03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index 655315b82..bb003fe4d 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -14,6 +14,7 @@ "com.unity.memoryprofiler": "0.7.1-preview.1", "com.unity.settings-manager": "1.0.3", "com.unity.test-framework": "1.1.31", + "com.unity.test-framework.performance": "3.0.2", "com.unity.testtools.codecoverage": "1.2.2", "com.unity.textmeshpro": "3.0.6", "com.unity.timeline": "1.6.4", @@ -51,5 +52,8 @@ "com.unity.modules.vr": "1.0.0", "com.unity.modules.wind": "1.0.0", "com.unity.modules.xr": "1.0.0" - } + }, + "testables": [ + "com.unity.test-framework.performance" + ] } diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index b2bf1cb6b..2fc17bbf1 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -184,6 +184,16 @@ }, "url": "https://packages.unity.com" }, + "com.unity.test-framework.performance": { + "version": "3.0.2", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.31", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, "com.unity.testtools.codecoverage": { "version": "1.2.2", "depth": 0,