Skip to content

Commit f0b61aa

Browse files
committed
feat: implement TextureFrame and TextureFramePool
1 parent dde2ee2 commit f0b61aa

File tree

13 files changed

+700
-96
lines changed

13 files changed

+700
-96
lines changed

Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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+
}

Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Experimental/TextureFrame.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)