CultLib/src/GameCult.Mesh/CultMeshFrameBodies.cs
2026-08-18 04:18:46 +02:00

982 lines
41 KiB
C#

using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;
#nullable enable
namespace GameCult.Mesh
{
/// <summary>Reports bounded shared-memory frame publication and explicit copy telemetry.</summary>
public sealed class CultMeshFrameBodyPublisherStats
{
internal CultMeshFrameBodyPublisherStats(
ulong publishedFrames,
ulong blockedWrites,
ulong unavoidableCopyCount)
{
PublishedFrames = publishedFrames;
BlockedWrites = blockedWrites;
UnavoidableCopyCount = unavoidableCopyCount;
}
/// <summary>Gets the number of atomically committed generations.</summary>
public ulong PublishedFrames { get; }
/// <summary>Gets the number of write reservations refused by bounded slots.</summary>
public ulong BlockedWrites { get; }
/// <summary>Gets copies explicitly declared by copy-fallback publication.</summary>
public ulong UnavoidableCopyCount { get; }
}
// Owner: this publisher owns mapping lifetime, logical generation, and slot commit.
// Consumers own only read leases; mappings, capabilities, and slot selection remain CultMesh state.
public sealed class CultMeshFrameBodyPublisher : IDisposable
{
internal const string CapabilityPrefix = "frame-v2-";
internal const int HeaderSize = 512;
internal const int SequenceOffset = 8;
internal const int ReaderCountOffset = 32;
internal const int WriterCountOffset = 48;
internal const int ContractHashOffset = 64;
internal const int ByteLengthOffset = 128;
internal const int LeaseExpiresAtOffset = 144;
internal const int SemanticHashOffset = 192;
internal const int SemanticHashStride = 64;
private readonly object _gate = new();
private readonly MemoryMappedFile _mapping;
private readonly MemoryMappedViewAccessor _control;
private readonly Mutex _mutex;
private readonly TimeSpan _leaseDuration;
private readonly string _token;
private readonly string? _mappingPath;
private int _cursor;
private long _nextSequence;
private ulong _publishedFrames;
private ulong _blockedWrites;
private ulong _unavoidableCopyCount;
private CultMeshFrameBodyWriteLease? _activeWrite;
private bool _disposed;
public CultMeshFrameBodyPublisher(
string bodyId,
string schemaId,
int layoutVersion,
int capacity,
long producerEpoch,
int slotByteLength,
TimeSpan? leaseDuration = null)
{
BodyId = Require(bodyId, nameof(bodyId));
SchemaId = Require(schemaId, nameof(schemaId));
if (layoutVersion < 0) throw new ArgumentOutOfRangeException(nameof(layoutVersion));
if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity));
if (producerEpoch < 0) throw new ArgumentOutOfRangeException(nameof(producerEpoch));
if (slotByteLength <= 0) throw new ArgumentOutOfRangeException(nameof(slotByteLength));
LayoutVersion = layoutVersion;
Capacity = capacity;
ProducerEpoch = producerEpoch;
SlotByteLength = slotByteLength;
_leaseDuration = leaseDuration ?? TimeSpan.FromSeconds(5);
if (_leaseDuration <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(leaseDuration));
_token = CapabilityPrefix + RandomToken();
var mappingByteLength = checked(HeaderSize + CultMeshFrameRegion.SlotCount * slotByteLength);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_mapping = MemoryMappedFile.CreateNew(MappingName(_token), mappingByteLength);
}
else
{
_mappingPath = MappingPath(_token);
_mapping = MemoryMappedFile.CreateFromFile(
_mappingPath,
FileMode.CreateNew,
null,
mappingByteLength,
MemoryMappedFileAccess.ReadWrite);
}
_control = _mapping.CreateViewAccessor(0, HeaderSize, MemoryMappedFileAccess.ReadWrite);
_control.Write(0, slotByteLength);
_control.Write(4, CultMeshFrameRegion.SlotCount);
for (var slot = 0; slot < CultMeshFrameRegion.SlotCount; slot++)
_control.Write(SequenceOffset + slot * sizeof(long), -1L);
var contractHash = Encoding.ASCII.GetBytes(ContractHash(BodyId, SchemaId, LayoutVersion, Capacity, ProducerEpoch, SlotByteLength));
_control.WriteArray(ContractHashOffset, contractHash, 0, contractHash.Length);
_mutex = new Mutex(false, MutexName(_token));
}
public string BodyId { get; }
public string SchemaId { get; }
public int LayoutVersion { get; }
public int Capacity { get; }
public long ProducerEpoch { get; }
public int SlotByteLength { get; }
public bool TryPublish(ReadOnlySpan<byte> body, DateTimeOffset nowUtc, out CultMeshBodyDescriptor descriptor)
{
if (body.Length > SlotByteLength) throw new ArgumentOutOfRangeException(nameof(body));
if (!TryAcquireWrite(out var write))
{
descriptor = null!;
return false;
}
using (write)
{
body.CopyTo(write.Span);
descriptor = write.Commit(body.Length, nowUtc, unavoidableCopyCount: 1);
return true;
}
}
/// <summary>
/// Reserves one producer-owned shared-memory slot for direct writes. The caller must commit or dispose the lease.
/// </summary>
public bool TryAcquireWrite(out CultMeshFrameBodyWriteLease lease)
{
lock (_gate)
{
ThrowIfDisposed();
if (_activeWrite != null)
{
_blockedWrites++;
lease = null!;
return false;
}
var slot = -1;
WithMutex(() =>
{
for (var attempt = 0; attempt < CultMeshFrameRegion.SlotCount; attempt++)
{
var candidate = (_cursor + attempt) % CultMeshFrameRegion.SlotCount;
if (_control.ReadInt32(ReaderCountOffset + candidate * sizeof(int)) != 0 ||
_control.ReadInt32(WriterCountOffset + candidate * sizeof(int)) != 0)
continue;
_control.Write(WriterCountOffset + candidate * sizeof(int), 1);
slot = candidate;
_cursor = (candidate + 1) % CultMeshFrameRegion.SlotCount;
return;
}
});
if (slot < 0)
{
_blockedWrites++;
lease = null!;
return false;
}
try
{
var view = _mapping.CreateViewAccessor(
SlotOffset(slot, SlotByteLength),
SlotByteLength,
MemoryMappedFileAccess.ReadWrite);
lease = new CultMeshFrameBodyWriteLease(this, view, slot, _nextSequence, SlotByteLength);
_activeWrite = lease;
return true;
}
catch
{
WithMutex(() => _control.Write(WriterCountOffset + slot * sizeof(int), 0));
throw;
}
}
}
internal CultMeshBodyDescriptor Commit(
CultMeshFrameBodyWriteLease lease,
int byteLength,
DateTimeOffset nowUtc,
int unavoidableCopyCount)
{
lock (_gate)
{
ThrowIfDisposed();
if (!ReferenceEquals(_activeWrite, lease) || lease.Sequence != _nextSequence)
throw new InvalidOperationException("The CultMesh frame write lease is stale or is not owned by this publisher.");
if (byteLength < 0 || byteLength > SlotByteLength)
throw new ArgumentOutOfRangeException(nameof(byteLength));
if (unavoidableCopyCount < 0)
throw new ArgumentOutOfRangeException(nameof(unavoidableCopyCount));
var semanticHash = CultMeshBodyDescriptorValidator.ComputeSemanticHash(lease.Span[..byteLength]);
WithMutex(() =>
{
var writerOffset = WriterCountOffset + lease.SlotIndex * sizeof(int);
if (_control.ReadInt32(writerOffset) != 1 ||
_control.ReadInt32(ReaderCountOffset + lease.SlotIndex * sizeof(int)) != 0)
throw new InvalidOperationException("The CultMesh frame write reservation was lost before commit.");
_control.Write(SequenceOffset + lease.SlotIndex * sizeof(long), lease.Sequence);
_control.Write(ByteLengthOffset + lease.SlotIndex * sizeof(int), byteLength);
_control.Write(LeaseExpiresAtOffset + lease.SlotIndex * sizeof(long), nowUtc.Add(_leaseDuration).ToUnixTimeMilliseconds());
var hashBytes = Encoding.ASCII.GetBytes(semanticHash);
_control.WriteArray(SemanticHashOffset + lease.SlotIndex * SemanticHashStride, hashBytes, 0, hashBytes.Length);
_control.Write(writerOffset, 0);
});
var descriptor = Descriptor(lease.SlotIndex, byteLength, lease.Sequence, semanticHash, nowUtc);
_nextSequence++;
_publishedFrames++;
_unavoidableCopyCount += (ulong)Math.Max(0, unavoidableCopyCount);
_activeWrite = null;
lease.Complete();
return descriptor;
}
}
internal void Abandon(CultMeshFrameBodyWriteLease lease)
{
lock (_gate)
{
if (!ReferenceEquals(_activeWrite, lease)) return;
if (!_disposed)
{
WithMutex(() =>
_control.Write(WriterCountOffset + lease.SlotIndex * sizeof(int), 0));
}
_activeWrite = null;
}
}
/// <summary>Gets current publication, backpressure, and explicit fallback-copy counters.</summary>
public CultMeshFrameBodyPublisherStats Stats()
{
lock (_gate)
{
ThrowIfDisposed();
return new CultMeshFrameBodyPublisherStats(
_publishedFrames,
_blockedWrites,
_unavoidableCopyCount);
}
}
private CultMeshBodyDescriptor Descriptor(
int slot,
int byteLength,
long sequence,
string semanticHash,
DateTimeOffset nowUtc) => new()
{
BodyId = BodyId, SchemaId = SchemaId, LayoutVersion = LayoutVersion, ByteSize = byteLength,
Capacity = Capacity, ProducerEpoch = ProducerEpoch, Sequence = sequence,
AccessMode = CultMeshBodyAccessMode.ReadOnly, Synchronization = CultMeshBodySynchronization.TripleBuffer,
LeaseExpiresAtUnixMs = nowUtc.Add(_leaseDuration).ToUnixTimeMilliseconds(),
TransportKind = CultMeshBodyTransportKind.SharedMemory,
CapabilityToken = _token + "." + slot,
SemanticHash = semanticHash
};
internal static bool IsFrameCapability(string capability) => capability.StartsWith(CapabilityPrefix, StringComparison.Ordinal);
internal static ICultMeshBodyReadLease OpenReadOnly(CultMeshBodyDescriptor descriptor)
{
ParseCapability(descriptor.CapabilityToken, out var token, out var slot);
MemoryMappedFile mapping;
Mutex mutex;
try
{
mapping = OpenMapping(token);
mutex = Mutex.OpenExisting(MutexName(token));
}
catch (Exception error) when (error is FileNotFoundException || error is WaitHandleCannotBeOpenedException)
{
throw new InvalidDataException("CultMesh frame capability is unavailable or revoked.", error);
}
try
{
int slotByteLength = 0;
WithMutex(mutex, () =>
{
using var control = mapping.CreateViewAccessor(0, HeaderSize, MemoryMappedFileAccess.ReadWrite);
slotByteLength = control.ReadInt32(0);
if (slot < 0 || slot >= control.ReadInt32(4) || descriptor.ByteSize > slotByteLength)
throw new InvalidDataException("CultMesh frame capability does not match its mapping layout.");
var contractHash = new byte[64];
control.ReadArray(ContractHashOffset, contractHash, 0, contractHash.Length);
var expectedContract = ContractHash(
descriptor.BodyId, descriptor.SchemaId, descriptor.LayoutVersion,
descriptor.Capacity, descriptor.ProducerEpoch, slotByteLength);
if (!string.Equals(Encoding.ASCII.GetString(contractHash), expectedContract, StringComparison.Ordinal) ||
control.ReadInt64(SequenceOffset + slot * sizeof(long)) != descriptor.Sequence)
throw new InvalidOperationException("CultMesh frame generation is stale or belongs to a different body contract.");
var offset = ReaderCountOffset + slot * sizeof(int);
control.Write(offset, checked(control.ReadInt32(offset) + 1));
});
return new CultMeshMappedBodyLease(
descriptor, mapping, SlotOffset(slot, slotByteLength),
() => ReleaseReader(token, slot));
}
catch
{
mapping.Dispose();
mutex.Dispose();
throw;
}
finally { mutex.Dispose(); }
}
internal static void ReleaseReader(string token, int slot)
{
try
{
using var mapping = OpenMapping(token);
using var mutex = Mutex.OpenExisting(MutexName(token));
WithMutex(mutex, () =>
{
using var control = mapping.CreateViewAccessor(0, HeaderSize, MemoryMappedFileAccess.ReadWrite);
var offset = ReaderCountOffset + slot * sizeof(int);
var readers = control.ReadInt32(offset);
if (readers > 0) control.Write(offset, readers - 1);
});
}
catch (Exception error) when (error is FileNotFoundException || error is WaitHandleCannotBeOpenedException) { }
}
public void Dispose()
{
lock (_gate)
{
if (_disposed) return;
_disposed = true;
_activeWrite?.Invalidate();
_activeWrite = null;
_control.Dispose();
_mapping.Dispose();
_mutex.Dispose();
if (_mappingPath != null)
{
try { File.Delete(_mappingPath); }
catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
}
}
private void WithMutex(Action action) => WithMutex(_mutex, action);
internal static void WithMutex(Mutex mutex, Action action)
{
mutex.WaitOne();
try { action(); }
finally { mutex.ReleaseMutex(); }
}
internal static long SlotOffset(int slot, int slotByteLength) => HeaderSize + (long)slot * slotByteLength;
internal static string MappingName(string token) => "cultmesh-map-" + token;
internal static string MappingPath(string token) => Path.Combine(Path.GetTempPath(), MappingName(token) + ".map");
internal static MemoryMappedFile OpenMapping(string token) =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? MemoryMappedFile.OpenExisting(MappingName(token), MemoryMappedFileRights.ReadWrite)
: MemoryMappedFile.CreateFromFile(MappingPath(token), FileMode.Open, null, 0, MemoryMappedFileAccess.ReadWrite);
internal static string MutexName(string token) => "cultmesh-lock-" + token;
internal static void ParseCapability(string capability, out string token, out int slot)
{
var separator = capability.LastIndexOf('.');
if (separator <= CapabilityPrefix.Length || !int.TryParse(capability.Substring(separator + 1), out slot))
throw new UnauthorizedAccessException("Invalid CultMesh frame capability.");
token = capability.Substring(0, separator);
}
private static string RandomToken()
{
var bytes = new byte[24];
using (var random = RandomNumberGenerator.Create()) random.GetBytes(bytes);
return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLowerInvariant();
}
internal static string ContractHash(string bodyId, string schemaId, int layoutVersion, int capacity, long epoch, int slotByteLength) =>
CultMeshBodyDescriptorValidator.ComputeSemanticHash(
Encoding.UTF8.GetBytes($"{bodyId}\n{schemaId}\n{layoutVersion}\n{capacity}\n{epoch}\n{slotByteLength}"));
private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(nameof(CultMeshFrameBodyPublisher)); }
private static string Require(string value, string name) => string.IsNullOrWhiteSpace(value) ? throw new ArgumentException("Value is required.", name) : value;
}
/// <summary>
/// Retains one verified shared-memory frame mapping and leases only newer committed generations.
/// The control plane is needed for bootstrap and layout changes, not for every frame.
/// </summary>
public sealed class CultMeshMappedFrameBodyCursor : IDisposable
{
private readonly CultMeshBodyDescriptor _contract;
private readonly string _token;
private readonly MemoryMappedFile _mapping;
private readonly MemoryMappedViewAccessor _control;
private readonly Mutex _mutex;
private readonly int _slotByteLength;
private long _lastSequence = -1;
private bool _disposed;
public static bool CanOpen(CultMeshBodyDescriptor descriptor) =>
descriptor != null &&
descriptor.TransportKind == CultMeshBodyTransportKind.SharedMemory &&
descriptor.Synchronization == CultMeshBodySynchronization.TripleBuffer &&
CultMeshFrameBodyPublisher.IsFrameCapability(descriptor.CapabilityToken);
public CultMeshMappedFrameBodyCursor(CultMeshBodyDescriptor descriptor)
{
_contract = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
if (!CanOpen(descriptor))
throw new ArgumentException("Descriptor is not a mapped CultMesh frame capability.", nameof(descriptor));
CultMeshFrameBodyPublisher.ParseCapability(descriptor.CapabilityToken, out _token, out _);
try
{
_mapping = CultMeshFrameBodyPublisher.OpenMapping(_token);
_mutex = Mutex.OpenExisting(CultMeshFrameBodyPublisher.MutexName(_token));
}
catch (Exception error) when (error is FileNotFoundException || error is WaitHandleCannotBeOpenedException)
{
throw new InvalidDataException("CultMesh frame capability is unavailable or revoked.", error);
}
try
{
_control = _mapping.CreateViewAccessor(
0,
CultMeshFrameBodyPublisher.HeaderSize,
MemoryMappedFileAccess.ReadWrite);
_slotByteLength = _control.ReadInt32(0);
var contractHash = new byte[64];
_control.ReadArray(CultMeshFrameBodyPublisher.ContractHashOffset, contractHash, 0, contractHash.Length);
var expected = CultMeshFrameBodyPublisher.ContractHash(
descriptor.BodyId,
descriptor.SchemaId,
descriptor.LayoutVersion,
descriptor.Capacity,
descriptor.ProducerEpoch,
_slotByteLength);
if (!string.Equals(Encoding.ASCII.GetString(contractHash), expected, StringComparison.Ordinal))
throw new InvalidDataException("CultMesh frame cursor contract does not match its mapping.");
}
catch
{
_mapping.Dispose();
_mutex.Dispose();
throw;
}
}
public long LastSequence => _lastSequence;
public bool TryAcquireLatest(out ICultMeshBodyReadLease lease) =>
TryAcquireLatest(DateTimeOffset.UtcNow, out lease);
public bool TryAcquireLatest(DateTimeOffset nowUtc, out ICultMeshBodyReadLease lease)
{
if (_disposed) throw new ObjectDisposedException(nameof(CultMeshMappedFrameBodyCursor));
var slot = -1;
var sequence = -1L;
var byteLength = 0;
var leaseExpiresAt = 0L;
var semanticHash = "";
CultMeshFrameBodyPublisher.WithMutex(_mutex, () =>
{
var slotCount = _control.ReadInt32(4);
for (var candidate = 0; candidate < slotCount; candidate++)
{
if (_control.ReadInt32(CultMeshFrameBodyPublisher.WriterCountOffset + candidate * sizeof(int)) != 0)
continue;
var candidateSequence = _control.ReadInt64(
CultMeshFrameBodyPublisher.SequenceOffset + candidate * sizeof(long));
if (candidateSequence <= _lastSequence || candidateSequence <= sequence)
continue;
var candidateExpiry = _control.ReadInt64(
CultMeshFrameBodyPublisher.LeaseExpiresAtOffset + candidate * sizeof(long));
var candidateLength = _control.ReadInt32(
CultMeshFrameBodyPublisher.ByteLengthOffset + candidate * sizeof(int));
// A byte-length change means the control-plane layout contract changed.
// Do not expose that generation through a cursor bootstrapped against
// the previous layout; its new descriptor must arrive first.
if (candidateExpiry <= nowUtc.ToUnixTimeMilliseconds() ||
candidateLength != _contract.ByteSize || candidateLength > _slotByteLength)
continue;
slot = candidate;
sequence = candidateSequence;
leaseExpiresAt = candidateExpiry;
byteLength = candidateLength;
}
if (slot < 0) return;
var hashBytes = new byte[CultMeshFrameBodyPublisher.SemanticHashStride];
_control.ReadArray(
CultMeshFrameBodyPublisher.SemanticHashOffset + slot * CultMeshFrameBodyPublisher.SemanticHashStride,
hashBytes,
0,
hashBytes.Length);
semanticHash = Encoding.ASCII.GetString(hashBytes);
var readerOffset = CultMeshFrameBodyPublisher.ReaderCountOffset + slot * sizeof(int);
_control.Write(readerOffset, checked(_control.ReadInt32(readerOffset) + 1));
});
if (slot < 0)
{
lease = null!;
return false;
}
var descriptor = new CultMeshBodyDescriptor
{
BodyId = _contract.BodyId,
SchemaId = _contract.SchemaId,
LayoutVersion = _contract.LayoutVersion,
ByteSize = byteLength,
Capacity = _contract.Capacity,
ProducerEpoch = _contract.ProducerEpoch,
Sequence = sequence,
AccessMode = CultMeshBodyAccessMode.ReadOnly,
Synchronization = CultMeshBodySynchronization.TripleBuffer,
LeaseExpiresAtUnixMs = leaseExpiresAt,
TransportKind = CultMeshBodyTransportKind.SharedMemory,
CapabilityToken = _token + "." + slot,
SemanticHash = semanticHash
};
try
{
var mapping = CultMeshFrameBodyPublisher.OpenMapping(_token);
lease = new CultMeshMappedBodyLease(
descriptor,
mapping,
CultMeshFrameBodyPublisher.SlotOffset(slot, _slotByteLength),
() => CultMeshFrameBodyPublisher.ReleaseReader(_token, slot));
_lastSequence = sequence;
return true;
}
catch
{
CultMeshFrameBodyPublisher.ReleaseReader(_token, slot);
throw;
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_control.Dispose();
_mapping.Dispose();
_mutex.Dispose();
}
}
/// <summary>A bounded producer lease over one directly writable shared-memory frame slot.</summary>
public sealed unsafe class CultMeshFrameBodyWriteLease : IDisposable
{
private CultMeshFrameBodyPublisher? _owner;
private readonly MemoryMappedViewAccessor _view;
private readonly CultMeshMappedMemoryManager _memory;
private bool _completed;
internal CultMeshFrameBodyWriteLease(
CultMeshFrameBodyPublisher owner,
MemoryMappedViewAccessor view,
int slotIndex,
long sequence,
int byteLength)
{
_owner = owner;
_view = view;
_memory = new CultMeshMappedMemoryManager(view, byteLength);
SlotIndex = slotIndex;
Sequence = sequence;
}
public int SlotIndex { get; }
public long Sequence { get; }
public Memory<byte> Memory => _memory.Memory;
public Span<byte> Span => Memory.Span;
public CultMeshBodyDescriptor Commit(
int byteLength,
DateTimeOffset nowUtc,
int unavoidableCopyCount = 0) =>
(_owner ?? throw new ObjectDisposedException(nameof(CultMeshFrameBodyWriteLease)))
.Commit(this, byteLength, nowUtc, unavoidableCopyCount);
internal void Complete()
{
_completed = true;
ReleaseResources();
}
internal void Invalidate()
{
_owner = null;
ReleaseResources();
}
public void Dispose()
{
var owner = Interlocked.Exchange(ref _owner, null);
if (!_completed) owner?.Abandon(this);
ReleaseResources();
}
private void ReleaseResources()
{
((IDisposable)_memory).Dispose();
_view.Dispose();
_owner = null;
}
}
internal sealed unsafe class CultMeshMappedMemoryManager : MemoryManager<byte>
{
private SafeMemoryMappedViewHandle? _handle;
private byte* _pointer;
private readonly int _length;
public CultMeshMappedMemoryManager(MemoryMappedViewAccessor view, int length)
{
_handle = view.SafeMemoryMappedViewHandle;
_length = length;
byte* pointer = null;
_handle.AcquirePointer(ref pointer);
_pointer = pointer + view.PointerOffset;
}
public override Span<byte> GetSpan() =>
_handle == null ? throw new ObjectDisposedException(nameof(CultMeshMappedMemoryManager)) : new Span<byte>(_pointer, _length);
public override MemoryHandle Pin(int elementIndex = 0)
{
if ((uint)elementIndex > (uint)_length) throw new ArgumentOutOfRangeException(nameof(elementIndex));
if (_handle == null) throw new ObjectDisposedException(nameof(CultMeshMappedMemoryManager));
return new MemoryHandle(_pointer + elementIndex);
}
public override void Unpin() { }
protected override void Dispose(bool disposing)
{
var handle = Interlocked.Exchange(ref _handle, null);
if (handle == null) return;
handle.ReleasePointer();
_pointer = null;
}
}
public sealed class CultMeshFrameGeneration
{
internal CultMeshFrameGeneration(
int slotIndex,
CultMeshBodyDescriptor descriptor,
long timestampNs,
long durationNs,
int unavoidableCopyCount,
IReadOnlyDictionary<string, string>? metadata)
{
SlotIndex = slotIndex;
Descriptor = descriptor;
TimestampNs = timestampNs;
DurationNs = durationNs;
UnavoidableCopyCount = unavoidableCopyCount;
Metadata = metadata == null
? new Dictionary<string, string>()
: new Dictionary<string, string>(metadata);
}
public int SlotIndex { get; }
public CultMeshBodyDescriptor Descriptor { get; }
public long TimestampNs { get; }
public long DurationNs { get; }
public int UnavoidableCopyCount { get; }
public IReadOnlyDictionary<string, string> Metadata { get; }
}
public readonly struct CultMeshFrameRegionWriteLease
{
private readonly CultMeshFrameRegion? _owner;
private readonly object? _reservation;
internal CultMeshFrameRegionWriteLease(
CultMeshFrameRegion owner,
object reservation,
int slotIndex,
long sequence,
Memory<byte> memory)
{
_owner = owner;
_reservation = reservation;
SlotIndex = slotIndex;
Sequence = sequence;
Memory = memory;
}
public int SlotIndex { get; }
public long Sequence { get; }
public Memory<byte> Memory { get; }
public Span<byte> Span => Memory.Span;
internal CultMeshFrameGeneration Commit(
int byteLength,
long timestampNs,
long durationNs,
int unavoidableCopyCount,
IReadOnlyDictionary<string, string>? metadata) =>
_owner?.Commit(this, _reservation!, byteLength, timestampNs, durationNs, unavoidableCopyCount, metadata)
?? throw new InvalidOperationException("The frame-region write lease is not valid.");
}
public sealed class CultMeshFrameRegionReadLease : IDisposable
{
private CultMeshFrameRegion? _owner;
internal CultMeshFrameRegionReadLease(
CultMeshFrameRegion owner,
CultMeshFrameGeneration generation,
ReadOnlyMemory<byte> memory)
{
_owner = owner;
Generation = generation;
Memory = memory;
}
public CultMeshFrameGeneration Generation { get; }
public ReadOnlyMemory<byte> Memory { get; }
public ReadOnlySpan<byte> Span => Memory.Span;
public void Dispose() => Interlocked.Exchange(ref _owner, null)?.ReleaseRead(Generation.SlotIndex);
}
public sealed class CultMeshFrameRegionStats
{
internal CultMeshFrameRegionStats(
ulong publishedFrames,
ulong droppedFrames,
ulong blockedWrites,
long latestSequence,
ulong unavoidableCopyCount)
{
PublishedFrames = publishedFrames;
DroppedFrames = droppedFrames;
BlockedWrites = blockedWrites;
LatestSequence = latestSequence;
UnavoidableCopyCount = unavoidableCopyCount;
}
public ulong PublishedFrames { get; }
public ulong DroppedFrames { get; }
public ulong BlockedWrites { get; }
public long LatestSequence { get; }
public ulong UnavoidableCopyCount { get; }
}
public sealed class CultMeshFrameRegion : IDisposable
{
public const int SlotCount = 3;
private readonly object _gate = new();
private readonly byte[] _storage;
private readonly int[] _readerCounts = new int[SlotCount];
private readonly object?[] _writeReservations = new object?[SlotCount];
private readonly CultMeshFrameGeneration?[] _generations = new CultMeshFrameGeneration?[SlotCount];
private object? _activeWriteReservation;
private int _writeCursor;
private int _latestSlot = -1;
private bool _disposed;
private long _nextSequence;
private ulong _publishedFrames;
private ulong _droppedFrames;
private ulong _blockedWrites;
private ulong _unavoidableCopyCount;
public CultMeshFrameRegion(
string bodyId,
string schemaId,
int layoutVersion,
int capacity,
long producerEpoch,
int slotByteLength)
{
BodyId = Require(bodyId, nameof(bodyId));
SchemaId = Require(schemaId, nameof(schemaId));
if (layoutVersion < 0) throw new ArgumentOutOfRangeException(nameof(layoutVersion));
if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity));
if (producerEpoch < 0) throw new ArgumentOutOfRangeException(nameof(producerEpoch));
if (slotByteLength <= 0) throw new ArgumentOutOfRangeException(nameof(slotByteLength));
LayoutVersion = layoutVersion;
Capacity = capacity;
ProducerEpoch = producerEpoch;
SlotByteLength = slotByteLength;
_storage = new byte[checked(SlotCount * slotByteLength)];
}
public string BodyId { get; }
public string SchemaId { get; }
public int LayoutVersion { get; }
public int Capacity { get; }
public long ProducerEpoch { get; }
public int SlotByteLength { get; }
public bool TryAcquireWrite(out CultMeshFrameRegionWriteLease lease)
{
lock (_gate)
{
ThrowIfDisposed();
if (_activeWriteReservation != null)
{
_blockedWrites++;
lease = default;
return false;
}
for (var attempt = 0; attempt < SlotCount; attempt++)
{
var slotIndex = (_writeCursor + attempt) % SlotCount;
if (_readerCounts[slotIndex] != 0 || _writeReservations[slotIndex] != null)
continue;
if (_generations[slotIndex] != null)
_droppedFrames++;
var reservation = new object();
_writeReservations[slotIndex] = reservation;
_activeWriteReservation = reservation;
_writeCursor = (slotIndex + 1) % SlotCount;
lease = new CultMeshFrameRegionWriteLease(
this,
reservation,
slotIndex,
_nextSequence,
new Memory<byte>(_storage, slotIndex * SlotByteLength, SlotByteLength));
return true;
}
_blockedWrites++;
lease = default;
return false;
}
}
public CultMeshFrameGeneration Commit(
CultMeshFrameRegionWriteLease lease,
int byteLength,
long timestampNs,
long durationNs = 0,
int unavoidableCopyCount = 0,
IReadOnlyDictionary<string, string>? metadata = null) =>
lease.Commit(byteLength, timestampNs, durationNs, unavoidableCopyCount, metadata);
internal CultMeshFrameGeneration Commit(
CultMeshFrameRegionWriteLease lease,
object reservation,
int byteLength,
long timestampNs,
long durationNs,
int unavoidableCopyCount,
IReadOnlyDictionary<string, string>? metadata)
{
lock (_gate)
{
ThrowIfDisposed();
RequireSlot(lease.SlotIndex);
if (!ReferenceEquals(_activeWriteReservation, reservation) ||
!ReferenceEquals(_writeReservations[lease.SlotIndex], reservation) ||
lease.Sequence != _nextSequence)
throw new InvalidOperationException("The frame-region write lease is stale or was already committed.");
if (byteLength < 0 || byteLength > SlotByteLength)
throw new ArgumentOutOfRangeException(nameof(byteLength));
var descriptor = new CultMeshBodyDescriptor
{
BodyId = BodyId,
SchemaId = SchemaId,
LayoutVersion = LayoutVersion,
ByteSize = byteLength,
Capacity = Capacity,
ProducerEpoch = ProducerEpoch,
Sequence = _nextSequence,
AccessMode = CultMeshBodyAccessMode.ReadOnly,
Synchronization = CultMeshBodySynchronization.TripleBuffer,
LeaseExpiresAtUnixMs = long.MaxValue,
TransportKind = CultMeshBodyTransportKind.SharedMemory,
SemanticHash = CultMeshBodyDescriptorValidator.ComputeSemanticHash(
lease.Span[..byteLength])
};
var generation = new CultMeshFrameGeneration(
lease.SlotIndex, descriptor, timestampNs, durationNs, unavoidableCopyCount, metadata);
_generations[lease.SlotIndex] = generation;
_writeReservations[lease.SlotIndex] = null;
_activeWriteReservation = null;
_latestSlot = lease.SlotIndex;
_nextSequence++;
_publishedFrames++;
_unavoidableCopyCount += (ulong)Math.Max(0, unavoidableCopyCount);
return generation;
}
}
public bool TryAcquireLatestRead(
CultMeshBodyValidationRequest request,
out CultMeshFrameRegionReadLease lease)
{
if (request == null) throw new ArgumentNullException(nameof(request));
lock (_gate)
{
ThrowIfDisposed();
if (_latestSlot < 0 || _generations[_latestSlot] == null)
{
lease = null!;
return false;
}
var generation = _generations[_latestSlot]!;
CultMeshBodyDescriptorValidator.Validate(generation.Descriptor, request);
_readerCounts[_latestSlot]++;
lease = new CultMeshFrameRegionReadLease(
this,
generation,
new ReadOnlyMemory<byte>(
_storage,
_latestSlot * SlotByteLength,
checked((int)generation.Descriptor.ByteSize)));
return true;
}
}
public CultMeshFrameRegionStats Stats()
{
lock (_gate)
{
ThrowIfDisposed();
return new CultMeshFrameRegionStats(
_publishedFrames,
_droppedFrames,
_blockedWrites,
_latestSlot < 0 ? 0 : _generations[_latestSlot]!.Descriptor.Sequence,
_unavoidableCopyCount);
}
}
internal void ReleaseRead(int slotIndex)
{
lock (_gate)
{
if (_disposed) return;
RequireSlot(slotIndex);
if (_readerCounts[slotIndex] <= 0)
throw new InvalidOperationException("The frame-region reader lease was released more than once.");
_readerCounts[slotIndex]--;
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed) return;
_disposed = true;
}
}
private void ThrowIfDisposed()
{
if (_disposed) throw new ObjectDisposedException(nameof(CultMeshFrameRegion));
}
private static string Require(string value, string parameterName) =>
string.IsNullOrWhiteSpace(value) ? throw new ArgumentException("Value is required.", parameterName) : value;
private static void RequireSlot(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= SlotCount) throw new ArgumentOutOfRangeException(nameof(slotIndex));
}
}
}