ParticleBuffer.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. // Copyright (c) Craftwork Games. All rights reserved.
  2. // Licensed under the MIT license.
  3. // See LICENSE file in the project root for full license information.
  4. using System;
  5. using System.Runtime.InteropServices;
  6. using MonoGame.Extended.Particles.Data;
  7. namespace MonoGame.Extended.Particles;
  8. /// <summary>
  9. /// Represents a circular memory buffer that efficiently stores and manages particles in contiguous unmanaged memory.
  10. /// </summary>
  11. /// <remarks>
  12. /// The <see cref="ParticleBuffer"/> class provides high-performance memory management for particle systems by
  13. /// allocating an unmanaged memory block to store particle data in a circular buffer arrangement. This implementation
  14. /// uses head and tail pointers to manage particle allocation and deallocation without requiring memory copying
  15. /// operations, making it more efficient than a linear buffer approach.
  16. /// </remarks>
  17. public unsafe class ParticleBuffer : IDisposable
  18. {
  19. private readonly ParticleIterator _iterator;
  20. /// <summary>
  21. /// Gets the native pointer to the beginning of the unmanaged memory buffer that stores particle data.
  22. /// </summary>
  23. /// <value>
  24. /// An <see cref="IntPtr"/> pointing to the start of the allocated unmanaged memory block.
  25. /// </value>
  26. /// <remarks>
  27. /// <para>
  28. /// This property provides direct access to the underlying unmanaged memory used by the particle buffer.
  29. /// The memory pointed to by this value contains particle data stored in a contiguous block, with each
  30. /// particle occupying <see cref="Particle.SizeInBytes"/> bytes.
  31. /// </para>
  32. /// <para>
  33. /// <strong>Warning:</strong> This pointer should be used with extreme caution and only within unsafe code blocks.
  34. /// Improper use of this pointer can lead to memory corruption, access violations, or other undefined behavior.
  35. /// The memory is automatically managed by this <see cref="ParticleBuffer"/> instance and should not be
  36. /// manually freed or modified outside of the provided safe methods.
  37. /// </para>
  38. /// <para>
  39. /// The memory layout is arranged as a circular buffer where particles are stored sequentially.
  40. /// Use <see cref="Head"/> and the <see cref="Iterator"/> for safe access to active particles rather than
  41. /// directly manipulating this pointer.
  42. /// </para>
  43. /// <para>
  44. /// This pointer becomes invalid after the <see cref="ParticleBuffer"/> is disposed. Accessing it after
  45. /// disposal will result in undefined behavior.
  46. /// </para>
  47. /// </remarks>
  48. public IntPtr NativePointer { get; }
  49. /// <summary>
  50. /// Gets a pointer to the current tail position in the circular buffer where new particles are allocated.
  51. /// </summary>
  52. public unsafe Particle* Tail { get; private set; }
  53. /// <summary>
  54. /// Gets a pointer to the end fo the allocated buffer memory, used for circular buffer bounds checking.
  55. /// </summary>
  56. public Particle* BufferEnd { get; }
  57. /// <summary>
  58. /// Gets the maximum number of particles that can be stored in this buffer.
  59. /// </summary>
  60. public int Size { get; }
  61. /// <summary>
  62. /// Gets an iterator for traversing the active particles in the buffer.
  63. /// </summary>
  64. /// <remarks>
  65. /// The iterator is reset each time this property is accessed, starting from the current head position.
  66. /// Use this to safely iterate through all active particles in the correct order.
  67. /// </remarks>
  68. public ParticleIterator Iterator => _iterator.Reset();
  69. /// <summary>
  70. /// Gets a pointer to the current head position in the circular buffer where the oldest active particle is located.
  71. /// </summary>
  72. /// <remarks>
  73. /// This pointer should be used carefully and only within unsafe code blocks. The memory it points to is
  74. /// automatically freed when the <see cref="ParticleBuffer"/> is disposed.
  75. /// </remarks>
  76. public unsafe Particle* Head { get; private set; }
  77. /// <summary>
  78. /// Gets the number of additional particles that can be added to the buffer before it is full.
  79. /// </summary>
  80. public int Available => Size - Count;
  81. /// <summary>
  82. /// Gets the current number of active particles in the buffer.
  83. /// </summary>
  84. public int Count { get; private set; }
  85. /// <summary>
  86. /// Gets the total size of the buffer in bytes.
  87. /// </summary>
  88. /// <remarks>
  89. /// The size includes space for one additional particle beyond the specified capacity to facilitate
  90. /// circular buffer operations.
  91. /// </remarks>
  92. public int SizeInBytes => Particle.SizeInBytes * (Size + 1);
  93. /// <summary>
  94. /// Gets the size of the currently active portion of the buffer in bytes.
  95. /// </summary>
  96. public int ActiveSizeInBytes => Particle.SizeInBytes * Count;
  97. /// <summary>
  98. /// Gets a value indicating whether this <see cref="ParticleBuffer"/> has been disposed.
  99. /// </summary>
  100. public bool IsDisposed { get; private set; }
  101. /// <summary>
  102. /// Initializes a new instance of the <see cref="ParticleBuffer"/> class with the specified capacity.
  103. /// </summary>
  104. /// <param name="size">The maximum number of particles that can be stored in the buffer.</param>
  105. public unsafe ParticleBuffer(int size)
  106. {
  107. Size = size;
  108. NativePointer = Marshal.AllocHGlobal(SizeInBytes);
  109. BufferEnd = (Particle*)(NativePointer + SizeInBytes);
  110. Head = (Particle*)NativePointer;
  111. Tail = (Particle*)NativePointer;
  112. _iterator = new ParticleIterator(this);
  113. GC.AddMemoryPressure(SizeInBytes);
  114. }
  115. /// <summary/>
  116. ~ParticleBuffer() => Dispose();
  117. /// <summary>
  118. /// Allocates space in the circular buffer for a specified number of particles to be released.
  119. /// </summary>
  120. /// <param name="releaseQuantity">The number of particles to allocate space for.</param>
  121. /// <returns>
  122. /// A <see cref="ParticleIterator"/> positioned at the start of the newly allocated particles,
  123. /// allowing iteration over the allocated particle slots.
  124. /// </returns>
  125. /// <exception cref="ObjectDisposedException">
  126. /// Thrown if this method is called after the buffer has been disposed.
  127. /// </exception>
  128. public unsafe ParticleIterator Release(int releaseQuantity)
  129. {
  130. ObjectDisposedException.ThrowIf(IsDisposed, this);
  131. int numToRelease = Math.Min(releaseQuantity, Available);
  132. int prevCount = Count;
  133. Count += numToRelease;
  134. Tail += numToRelease;
  135. if (Tail >= BufferEnd)
  136. {
  137. Tail -= Size + 1;
  138. }
  139. return Iterator.Reset(prevCount);
  140. }
  141. /// <summary>
  142. /// Removes a specified number of particles from the beginning of the circular buffer.
  143. /// </summary>
  144. /// <param name="number">The number of particles to remove from the buffer.</param>
  145. /// <exception cref="ObjectDisposedException">
  146. /// Thrown if this method is called after the buffer has been disposed.
  147. /// </exception>
  148. public unsafe void Reclaim(int number)
  149. {
  150. ObjectDisposedException.ThrowIf(IsDisposed, this);
  151. Count -= number;
  152. Head += number;
  153. if (Head >= BufferEnd)
  154. {
  155. Head -= Size + 1;
  156. }
  157. }
  158. /// <summary>
  159. /// Releases all resources used by the <see cref="ParticleBuffer"/>.
  160. /// </summary>
  161. public void Dispose()
  162. {
  163. if (IsDisposed)
  164. {
  165. return;
  166. }
  167. Marshal.FreeHGlobal(NativePointer);
  168. GC.RemoveMemoryPressure(SizeInBytes);
  169. IsDisposed = true;
  170. GC.SuppressFinalize(this);
  171. }
  172. }