diff --git a/K4AdotNet.Tests.Unit/Sensor/ImageTests.cs b/K4AdotNet.Tests.Unit/Sensor/ImageTests.cs index 122eb6e..ab487b6 100644 --- a/K4AdotNet.Tests.Unit/Sensor/ImageTests.cs +++ b/K4AdotNet.Tests.Unit/Sensor/ImageTests.cs @@ -426,5 +426,95 @@ public void TestImageSizeCalculationCustom() } #endregion + + #region Test custom memory management + + private sealed class TestMemoryAllocator : ICustomMemoryAllocator + { + public int AllocateCount { get; private set; } + public nint AllocContextValue { get; set; } + public int LastAllocSizeValue { get; private set; } + public nint LastAllocReturnValue { get; private set; } + + public int FreeCount { get; private set; } + public nint LastFreeContextValue { get; private set; } + + nint ICustomMemoryAllocator.Allocate(int size, out nint context) + { + AllocateCount++; + context = AllocContextValue; + LastAllocSizeValue = size; + return LastAllocReturnValue = Marshal.AllocHGlobal(size); + } + + void ICustomMemoryAllocator.Free(nint buffer, nint context) + { + FreeCount++; + LastFreeContextValue = context; + Marshal.FreeHGlobal(buffer); + } + } + + [TestMethod] + public void TestCustomMemoryManagement() + { + // Set our test custom allocator + var testAllocator = new TestMemoryAllocator(); + Sdk.SetCustomMemoryAllocator(testAllocator); + // Check initial state + Assert.AreEqual(0, testAllocator.AllocateCount); + Assert.AreEqual(0, testAllocator.FreeCount); + + // The first test image - should result in one memory allocation + var testContextA = testAllocator.AllocContextValue = 12345; + var testImageA = new Image(ImageFormat.Depth16, 1, 1); + // One allocation but no calls of Free + Assert.AreEqual(1, testAllocator.AllocateCount); // 0 -> 1 ! + Assert.AreEqual(0, testAllocator.FreeCount); // unchanged + // SDK can request bigger buffer and place actual image buffer somewhere inside allocated one + Assert.IsTrue(testAllocator.LastAllocSizeValue >= 2); + Assert.IsTrue(testAllocator.LastAllocReturnValue <= testImageA.Buffer); + Assert.IsTrue(testAllocator.LastAllocReturnValue + testAllocator.LastAllocSizeValue >= testImageA.Buffer + 2); + + // The second test image - should result in one more memory allocation + var testContextB = testAllocator.AllocContextValue = 98765; + var testImageB = new Image(ImageFormat.ColorBgra32, 1, 1); + // Two allocations but still no calls of Free + Assert.AreEqual(2, testAllocator.AllocateCount); // 1 -> 2 ! + Assert.AreEqual(0, testAllocator.FreeCount); // unchanged + // SDK can request bigger buffer and place actual image buffer somewhere inside allocated one + Assert.IsTrue(testAllocator.LastAllocSizeValue >= 4); + Assert.IsTrue(testAllocator.LastAllocReturnValue <= testImageB.Buffer); + Assert.IsTrue(testAllocator.LastAllocReturnValue + testAllocator.LastAllocSizeValue >= testImageB.Buffer + 4); + + // Disposing of the first test image - should result in appropriate call of Free method + testImageA.Dispose(); + // Now, one call to Free + Assert.AreEqual(2, testAllocator.AllocateCount); // unchanged + Assert.AreEqual(1, testAllocator.FreeCount); // 0 -> 1 ! + Assert.AreEqual(testContextA, testAllocator.LastFreeContextValue); + + // Clear custom allocator + Sdk.SetCustomMemoryAllocator(null); + + // Now creation of test image does not result in calls to our testAllocator instance + var testImageC = new Image(ImageFormat.ColorYUY2, testWidth, testHeight); + Assert.AreEqual(2, testAllocator.AllocateCount); + Assert.AreEqual(1, testAllocator.FreeCount); + + // The second test image was created with the aid of out test allocator, + // this why it should be releasing using the same allocator in spite of the fact that this allocator is not active anymore + testImageB.Dispose(); + Assert.AreEqual(2, testAllocator.AllocateCount); // unchanged + Assert.AreEqual(2, testAllocator.FreeCount); // 1 -> 2 ! + Assert.AreEqual(testContextB, testAllocator.LastFreeContextValue); + + // But for the testImageC our testAllocator shouldn't be called on releasing either + testImageC.Dispose(); + Assert.AreEqual(2, testAllocator.AllocateCount); // unchanged + Assert.AreEqual(2, testAllocator.FreeCount); // unchanged value! + } + + #endregion } } diff --git a/K4AdotNet/ICustomMemoryAllocator.cs b/K4AdotNet/ICustomMemoryAllocator.cs new file mode 100644 index 0000000..63d6113 --- /dev/null +++ b/K4AdotNet/ICustomMemoryAllocator.cs @@ -0,0 +1,22 @@ +using System; + +namespace K4AdotNet +{ + /// + /// Base interface for custom memory manager that can be used to allocate and free memory used by images. + /// + /// + public interface ICustomMemoryAllocator + { + /// Function for a memory allocation. Will be called by internals of Azure Kinect SDK. + /// Minimum size in bytes needed for the buffer. + /// Output parameter for a context that will be provided in the subsequent call to the callback. + /// A pointer to the newly allocated memory. + IntPtr Allocate(int size, out IntPtr context); + + /// Function for a memory object being destroyed. Will be called by internals of Azure Kinect SDK. + /// The buffer pointer that was supplied by the method and that should be free. + /// The context for the memory object that needs to be destroyed that was supplied by the method. + void Free(IntPtr buffer, IntPtr context); + } +} diff --git a/K4AdotNet/Sdk.cs b/K4AdotNet/Sdk.cs index e5cf7ce..408ae09 100644 --- a/K4AdotNet/Sdk.cs +++ b/K4AdotNet/Sdk.cs @@ -1,10 +1,10 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Reflection; [assembly: CLSCompliant(isCompliant: true)] @@ -144,6 +144,48 @@ public static void ConfigureBodyTrackingLogging(TraceLevel level, bool logToStdo #endregion + #region Custom memory allocation + + // to keep callbacks alive + private static readonly ConcurrentBag allocateCallbacks = new(); + private static readonly ConcurrentBag destroyCallbacks = new(); + + /// + /// Sets or clears custom memory allocator to be use by internals of Azure Kinect SDK. + /// + /// + /// Instance of to allocate and free memory in internals of Azure Kinect SDK + /// or to "clear" custom memory allocator (that is, to switch to standard built-in SDK's allocator). + /// + /// Setting or clearing of custom memory allocator was failed. See log for details. + /// + /// All instances of will be keeping alive forever because they can be used + /// to free memory even after setting custom allocator to . + /// + public static void SetCustomMemoryAllocator(ICustomMemoryAllocator? allocator) + { + if (allocator is null) + { + var res = Sensor.NativeApi.SetAllocator(null, null); + if (res != NativeCallResults.Result.Succeeded) + throw new InvalidOperationException("Cannot clear custom memory allocator"); + return; + } + + var allocateCallback = new Sensor.NativeApi.MemoryAllocateCallback(allocator.Allocate); + var destroyCallback = new Sensor.NativeApi.MemoryDestroyCallback(allocator.Free); + + // to keep callbacks alive + allocateCallbacks.Add(allocateCallback); + destroyCallbacks.Add(destroyCallback); + + var rs = Sensor.NativeApi.SetAllocator(allocateCallback, destroyCallback); + if (rs != NativeCallResults.Result.Succeeded) + throw new InvalidOperationException("Cannot set custom memory allocator"); + } + + #endregion + #region Body tracking SDK availability and initialization /// URL to step-by-step instruction "How to set up Body Tracking SDK". Helpful for UI and user messages. @@ -278,7 +320,7 @@ private static bool TryGetBodyTrackingRuntimePath( #if NET461 || NETSTANDARD2_0 // Try location of this assembly - var asm = Assembly.GetExecutingAssembly(); + var asm = System.Reflection.Assembly.GetExecutingAssembly(); var asmDir = Path.GetFullPath(Path.GetDirectoryName(new Uri(asm.GetName().CodeBase!).LocalPath)!); if (!asmDir.Equals(currentDir, StringComparison.InvariantCultureIgnoreCase) && !asmDir.Equals(baseDir, StringComparison.InvariantCultureIgnoreCase)) diff --git a/K4AdotNet/Sensor/NativeApi.cs b/K4AdotNet/Sensor/NativeApi.cs index 46d2cc5..b2d849b 100644 --- a/K4AdotNet/Sensor/NativeApi.cs +++ b/K4AdotNet/Sensor/NativeApi.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; namespace K4AdotNet.Sensor @@ -187,6 +188,15 @@ public static extern NativeCallResults.Result ImageCreate( int strideBytes, out NativeHandles.ImageHandle imageHandle); + // typedef uint8_t *(k4a_memory_allocate_cb_t)(int size, void **context); + /// Callback function for a memory allocation. + /// Minimum size in bytes needed for the buffer. + /// Output parameter for a context that will be provided in the subsequent call to the callback. + /// A pointer to the newly allocated memory. + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate IntPtr MemoryAllocateCallback(int size, out IntPtr context); + + // typedef void(k4a_memory_destroy_cb_t)(void *buffer, void *context); /// Callback function for a memory object being destroyed. /// The buffer pointer that was supplied by the caller. @@ -194,6 +204,35 @@ public static extern NativeCallResults.Result ImageCreate( [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void MemoryDestroyCallback(IntPtr buffer, IntPtr context); + // K4A_EXPORT k4a_result_t k4a_set_allocator(k4a_memory_allocate_cb_t allocate, k4a_memory_destroy_cb_t free); + /// Sets the callback functions for the SDK allocator + /// + /// The callback function to allocate memory. When the SDK requires memory allocation this callback will be + /// called and the application can provide a buffer and a context. + /// + /// + /// The callback function to free memory. + /// The SDK will call this function when memory allocated by is no longer needed. + /// + /// if the callback function was set or cleared successfully. + /// if an error is encountered or the callback function has already been set. + /// + /// + /// Call this function to hook memory allocation by the SDK. Calling with both and + /// as will clear the hook and reset to the default allocator. + /// + /// If this function is called after memory has been allocated, the previous version of function may still be + /// called in the future. The SDK will always call the function that was set at the time that the memory + /// was allocated. + /// + /// Not all memory allocation by the SDK is performed by this allocate function. + /// Small allocations or allocations from special pools may come from other sources. + /// + [DllImport(Sdk.SENSOR_DLL_NAME, EntryPoint = "k4a_set_allocator", CallingConvention = CallingConvention.Cdecl)] + public static extern NativeCallResults.Result SetAllocator( + MemoryAllocateCallback? allocate, + MemoryDestroyCallback? free); + // K4A_EXPORT k4a_result_t k4a_image_create_from_buffer(k4a_image_format_t format, // int width_pixels, // int height_pixels,