|
| 1 | +// Copyright (c) 2023 homuler |
| 2 | +// |
| 3 | +// Use of this source code is governed by an MIT-style |
| 4 | +// license that can be found in the LICENSE file or at |
| 5 | +// https://opensource.org/licenses/MIT. |
| 6 | + |
| 7 | +using System; |
| 8 | +using System.Collections; |
| 9 | +using System.Collections.Generic; |
| 10 | +using Unity.Collections; |
| 11 | +using UnityEngine; |
| 12 | +using UnityEngine.Events; |
| 13 | +using UnityEngine.Experimental.Rendering; |
| 14 | +using UnityEngine.Rendering; |
| 15 | + |
| 16 | +namespace Mediapipe.Unity.Experimental |
| 17 | +{ |
| 18 | +#pragma warning disable IDE0065 |
| 19 | + using Color = UnityEngine.Color; |
| 20 | +#pragma warning restore IDE0065 |
| 21 | + |
| 22 | + public class TextureFrame : IDisposable |
| 23 | + { |
| 24 | + public class ReleaseEvent : UnityEvent<TextureFrame> { } |
| 25 | + |
| 26 | + private const string _TAG = nameof(TextureFrame); |
| 27 | + |
| 28 | + internal const int MaxTotalCount = 100; |
| 29 | + |
| 30 | + private static readonly GlobalInstanceTable<Guid, TextureFrame> _InstanceTable = new GlobalInstanceTable<Guid, TextureFrame>(MaxTotalCount); |
| 31 | + /// <summary> |
| 32 | + /// A dictionary to look up which native texture belongs to which <see cref="TextureFrame" />. |
| 33 | + /// </summary> |
| 34 | + /// <remarks> |
| 35 | + /// Not all the <see cref="TextureFrame" /> instances are registered. |
| 36 | + /// Texture names are queried only when necessary, and the corresponding data will be saved then. |
| 37 | + /// </remarks> |
| 38 | + private static readonly Dictionary<uint, Guid> _NameTable = new Dictionary<uint, Guid>(); |
| 39 | + |
| 40 | + private readonly Texture2D _texture; |
| 41 | + public Texture texture => _texture; |
| 42 | + |
| 43 | + private IntPtr _nativeTexturePtr = IntPtr.Zero; |
| 44 | + private GlSyncPoint _glSyncToken; |
| 45 | + |
| 46 | + private readonly Guid _instanceId; |
| 47 | + // NOTE: width and height can be accessed from a thread other than Main Thread. |
| 48 | + public readonly int width; |
| 49 | + public readonly int height; |
| 50 | + public readonly TextureFormat format; |
| 51 | + |
| 52 | + public ImageFormat.Types.Format imageFormat => format.ToImageFormat(); |
| 53 | + |
| 54 | + public bool isReadable => _texture.isReadable; |
| 55 | + |
| 56 | + // TODO: determine at runtime |
| 57 | + public GpuBufferFormat gpuBufferformat => GpuBufferFormat.kBGRA32; |
| 58 | + |
| 59 | + /// <summary> |
| 60 | + /// The event that will be invoked when the TextureFrame is released. |
| 61 | + /// </summary> |
| 62 | +#pragma warning disable IDE1006 // UnityEvent is PascalCase |
| 63 | + public readonly ReleaseEvent OnRelease; |
| 64 | +#pragma warning restore IDE1006 |
| 65 | + |
| 66 | + private TextureFrame(Texture2D texture) |
| 67 | + { |
| 68 | + _texture = texture; |
| 69 | + width = texture.width; |
| 70 | + height = texture.height; |
| 71 | + format = texture.format; |
| 72 | + OnRelease = new ReleaseEvent(); |
| 73 | + _instanceId = Guid.NewGuid(); |
| 74 | + _InstanceTable.Add(_instanceId, this); |
| 75 | + } |
| 76 | + |
| 77 | + public TextureFrame(int width, int height, TextureFormat format) : this(new Texture2D(width, height, format, false)) { } |
| 78 | + public TextureFrame(int width, int height) : this(width, height, TextureFormat.RGBA32) { } |
| 79 | + |
| 80 | + public void Dispose() |
| 81 | + { |
| 82 | + RemoveAllReleaseListeners(); |
| 83 | + if (_nativeTexturePtr != IntPtr.Zero) |
| 84 | + { |
| 85 | + var name = (uint)_nativeTexturePtr; |
| 86 | + lock (((ICollection)_NameTable).SyncRoot) |
| 87 | + { |
| 88 | + var _ = _NameTable.Remove(name); |
| 89 | + } |
| 90 | + } |
| 91 | + _glSyncToken?.Dispose(); |
| 92 | + } |
| 93 | + |
| 94 | + public void CopyTexture(Texture dst) => Graphics.CopyTexture(_texture, dst); |
| 95 | + |
| 96 | + public void CopyTextureFrom(Texture src) => Graphics.CopyTexture(src, _texture); |
| 97 | + |
| 98 | + public bool ConvertTexture(Texture dst) => Graphics.ConvertTexture(_texture, dst); |
| 99 | + |
| 100 | + public bool ConvertTextureFrom(Texture src) => Graphics.ConvertTexture(src, _texture); |
| 101 | + |
| 102 | + /// <summary> |
| 103 | + /// Copy texture data from <paramref name="src" />. |
| 104 | + /// If <paramref name="src" /> format is different from <see cref="format" />, it converts the format. |
| 105 | + /// </summary> |
| 106 | + /// <remarks> |
| 107 | + /// After calling it, pixel data can't be read from CPU safely. |
| 108 | + /// </remarks> |
| 109 | + public bool ReadTextureOnGPU(Texture src) |
| 110 | + { |
| 111 | + if (GetTextureFormat(src) != format) |
| 112 | + { |
| 113 | + return Graphics.ConvertTexture(src, _texture); |
| 114 | + } |
| 115 | + Graphics.CopyTexture(src, _texture); |
| 116 | + return true; |
| 117 | + } |
| 118 | + |
| 119 | + public AsyncGPUReadbackRequest ReadTextureAsync(Texture src) |
| 120 | + { |
| 121 | + if (!ReadTextureOnGPU(src)) |
| 122 | + { |
| 123 | + throw new InvalidOperationException("Failed to read texture on GPU"); |
| 124 | + } |
| 125 | + |
| 126 | + return AsyncGPUReadback.Request(_texture, 0); |
| 127 | + } |
| 128 | + |
| 129 | + public AsyncGPUReadbackRequest ReadTextureAsync(Texture src, bool flipVertically, bool flipHorizontally) |
| 130 | + { |
| 131 | + var tmpRenderTexture = RenderTexture.GetTemporary(src.width, src.height, 32); |
| 132 | + var currentRenderTexture = RenderTexture.active; |
| 133 | + RenderTexture.active = tmpRenderTexture; |
| 134 | + |
| 135 | + var scale = new Vector2(1.0f, 1.0f); |
| 136 | + var offset = new Vector2(0.0f, 0.0f); |
| 137 | + if (flipVertically) |
| 138 | + { |
| 139 | + scale.y = -1.0f; |
| 140 | + offset.y = 1.0f; |
| 141 | + } |
| 142 | + if (flipHorizontally) |
| 143 | + { |
| 144 | + scale.x = -1.0f; |
| 145 | + offset.x = 1.0f; |
| 146 | + } |
| 147 | + Graphics.Blit(src, tmpRenderTexture, scale, offset); |
| 148 | + |
| 149 | + RenderTexture.active = currentRenderTexture; |
| 150 | + |
| 151 | + return AsyncGPUReadback.Request(tmpRenderTexture, 0, (req) => |
| 152 | + { |
| 153 | + _texture.LoadRawTextureData(req.GetData<byte>()); |
| 154 | + _texture.Apply(); |
| 155 | + _ = RevokeNativeTexturePtr(); |
| 156 | + RenderTexture.ReleaseTemporary(tmpRenderTexture); |
| 157 | + }); |
| 158 | + } |
| 159 | + |
| 160 | + public Color GetPixel(int x, int y) => _texture.GetPixel(x, y); |
| 161 | + |
| 162 | + public Color32[] GetPixels32() => _texture.GetPixels32(); |
| 163 | + |
| 164 | + public void SetPixels32(Color32[] pixels) |
| 165 | + { |
| 166 | + _texture.SetPixels32(pixels); |
| 167 | + _texture.Apply(); |
| 168 | + |
| 169 | + if (!RevokeNativeTexturePtr()) |
| 170 | + { |
| 171 | + // If this line was executed, there must be a bug. |
| 172 | + Logger.LogError("Failed to revoke the native texture."); |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + public NativeArray<T> GetRawTextureData<T>() where T : struct => _texture.GetRawTextureData<T>(); |
| 177 | + |
| 178 | + /// <returns>The texture's native pointer</returns> |
| 179 | + public IntPtr GetNativeTexturePtr() |
| 180 | + { |
| 181 | + if (_nativeTexturePtr == IntPtr.Zero) |
| 182 | + { |
| 183 | + _nativeTexturePtr = _texture.GetNativeTexturePtr(); |
| 184 | + var name = (uint)_nativeTexturePtr; |
| 185 | + |
| 186 | + lock (((ICollection)_NameTable).SyncRoot) |
| 187 | + { |
| 188 | + if (!AcquireName(name, _instanceId)) |
| 189 | + { |
| 190 | + throw new InvalidProgramException($"Another instance (id={_instanceId}) is using the specified name ({name}) now"); |
| 191 | + } |
| 192 | + _NameTable.Add(name, _instanceId); |
| 193 | + } |
| 194 | + } |
| 195 | + return _nativeTexturePtr; |
| 196 | + } |
| 197 | + |
| 198 | + public uint GetTextureName() => (uint)GetNativeTexturePtr(); |
| 199 | + |
| 200 | + public Guid GetInstanceID() => _instanceId; |
| 201 | + |
| 202 | + public ImageFrame BuildImageFrame() => new ImageFrame(imageFormat, width, height, 4 * width, GetRawTextureData<byte>()); |
| 203 | + |
| 204 | + public GpuBuffer BuildGpuBuffer(GlContext glContext) |
| 205 | + { |
| 206 | +#if UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX || UNITY_ANDROID |
| 207 | + var glTextureBuffer = new GlTextureBuffer(GetTextureName(), width, height, gpuBufferformat, OnReleaseTextureFrame, glContext); |
| 208 | + return new GpuBuffer(glTextureBuffer); |
| 209 | +#else |
| 210 | + throw new NotSupportedException("This method is only supported on Linux or Android"); |
| 211 | +#endif |
| 212 | + } |
| 213 | + |
| 214 | + public void RemoveAllReleaseListeners() => OnRelease.RemoveAllListeners(); |
| 215 | + |
| 216 | + // TODO: stop invoking OnRelease when it's already released |
| 217 | + public void Release(GlSyncPoint token = null) |
| 218 | + { |
| 219 | + _glSyncToken?.Dispose(); |
| 220 | + _glSyncToken = token; |
| 221 | + OnRelease.Invoke(this); |
| 222 | + } |
| 223 | + |
| 224 | + /// <summary> |
| 225 | + /// Waits until the GPU has executed all commands up to the sync point. |
| 226 | + /// This blocks the CPU, and ensures the commands are complete from the point of view of all threads and contexts. |
| 227 | + /// </summary> |
| 228 | + public void WaitUntilReleased() |
| 229 | + { |
| 230 | + if (_glSyncToken == null) |
| 231 | + { |
| 232 | + return; |
| 233 | + } |
| 234 | + _glSyncToken.Wait(); |
| 235 | + _glSyncToken.Dispose(); |
| 236 | + _glSyncToken = null; |
| 237 | + } |
| 238 | + |
| 239 | + [AOT.MonoPInvokeCallback(typeof(GlTextureBuffer.DeletionCallback))] |
| 240 | + public static void OnReleaseTextureFrame(uint textureName, IntPtr syncTokenPtr) |
| 241 | + { |
| 242 | + var isIdFound = _NameTable.TryGetValue(textureName, out var _instanceId); |
| 243 | + |
| 244 | + if (!isIdFound) |
| 245 | + { |
| 246 | + Logger.LogError(_TAG, $"nameof (name={textureName}) is released, but the owner TextureFrame is not found"); |
| 247 | + return; |
| 248 | + } |
| 249 | + |
| 250 | + var isTextureFrameFound = _InstanceTable.TryGetValue(_instanceId, out var textureFrame); |
| 251 | + |
| 252 | + if (!isTextureFrameFound) |
| 253 | + { |
| 254 | + Logger.LogWarning(_TAG, $"nameof owner TextureFrame of the released texture (name={textureName}) is already garbage collected"); |
| 255 | + return; |
| 256 | + } |
| 257 | + |
| 258 | + var _glSyncToken = syncTokenPtr == IntPtr.Zero ? null : new GlSyncPoint(syncTokenPtr); |
| 259 | + textureFrame.Release(_glSyncToken); |
| 260 | + } |
| 261 | + |
| 262 | + /// <summary> |
| 263 | + /// Remove <paramref name="name" /> from <see cref="_NameTable" /> if it's stale. |
| 264 | + /// If <paramref name="name" /> does not exist in <see cref="_NameTable" />, do nothing. |
| 265 | + /// </summary> |
| 266 | + /// <remarks> |
| 267 | + /// If the instance whose id is <paramref name="ownerId" /> owns <paramref name="name" /> now, it still removes <paramref name="name" />. |
| 268 | + /// </remarks> |
| 269 | + /// <returns>Return if name is available</returns> |
| 270 | + private static bool AcquireName(uint name, Guid ownerId) |
| 271 | + { |
| 272 | + if (_NameTable.TryGetValue(name, out var id)) |
| 273 | + { |
| 274 | + if (ownerId != id && _InstanceTable.TryGetValue(id, out var _)) |
| 275 | + { |
| 276 | + // if instance is found, the instance is using the name. |
| 277 | + Logger.LogVerbose($"{id} is using {name} now"); |
| 278 | + return false; |
| 279 | + } |
| 280 | + var _ = _NameTable.Remove(name); |
| 281 | + } |
| 282 | + return true; |
| 283 | + } |
| 284 | + |
| 285 | + private static TextureFormat GetTextureFormat(Texture texture) => GraphicsFormatUtility.GetTextureFormat(texture.graphicsFormat); |
| 286 | + |
| 287 | + /// <summary> |
| 288 | + /// Remove the texture name from <see cref="_NameTable" /> and empty <see cref="_nativeTexturePtr" />. |
| 289 | + /// This method needs to be called when an operation is performed that may change the internal texture. |
| 290 | + /// </summary> |
| 291 | + private bool RevokeNativeTexturePtr() |
| 292 | + { |
| 293 | + if (_nativeTexturePtr == IntPtr.Zero) |
| 294 | + { |
| 295 | + return true; |
| 296 | + } |
| 297 | + |
| 298 | + var currentName = GetTextureName(); |
| 299 | + if (!_NameTable.Remove(currentName)) |
| 300 | + { |
| 301 | + return false; |
| 302 | + } |
| 303 | + _nativeTexturePtr = IntPtr.Zero; |
| 304 | + return true; |
| 305 | + } |
| 306 | + } |
| 307 | +} |
0 commit comments