Add C# CultMesh streaming surface
This commit is contained in:
parent
67d3bf3dcf
commit
f7182159b4
8 changed files with 1001 additions and 11 deletions
|
|
@ -212,6 +212,14 @@ namespace GameCult.Mesh
|
|||
return new CultMeshAuthorityLeaseCatalog();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a local stream catalog for zero-copy-oriented audio, video, tensor, and byte frame streams.
|
||||
/// </summary>
|
||||
public static CultMeshStreamCatalog CreateStreamCatalog()
|
||||
{
|
||||
return new CultMeshStreamCatalog();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a committer for writing quorum simulation facts to a mesh database.
|
||||
/// </summary>
|
||||
|
|
|
|||
711
src/GameCult.Mesh/CultMeshStreaming.cs
Normal file
711
src/GameCult.Mesh/CultMeshStreaming.cs
Normal file
|
|
@ -0,0 +1,711 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using static GameCult.Mesh.CultMeshGuards;
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace GameCult.Mesh
|
||||
{
|
||||
public enum CultMeshStreamKind
|
||||
{
|
||||
Audio,
|
||||
Video,
|
||||
Tensor,
|
||||
Bytes
|
||||
}
|
||||
|
||||
public enum CultMeshStreamBodyTransport
|
||||
{
|
||||
SharedMemory,
|
||||
SharedD3D12Texture,
|
||||
SharedD3D11Texture,
|
||||
DmaBuf,
|
||||
IOSurface,
|
||||
AHardwareBuffer,
|
||||
CultCachePage,
|
||||
InlineBytes
|
||||
}
|
||||
|
||||
public enum CultMeshStreamAccess
|
||||
{
|
||||
Read,
|
||||
Write,
|
||||
ReadWrite
|
||||
}
|
||||
|
||||
public enum CultMeshStreamCopyBudget
|
||||
{
|
||||
ZeroCopyTarget,
|
||||
OneCopyFallback,
|
||||
OpaqueRuntime
|
||||
}
|
||||
|
||||
public sealed class CultMeshStreamClock
|
||||
{
|
||||
public CultMeshStreamClock(
|
||||
string clockDomainId,
|
||||
string? sourceId = null,
|
||||
int sampleRate = 0,
|
||||
long offsetToVerseTimeNs = 0,
|
||||
double confidence = 0,
|
||||
string? evidenceKind = null)
|
||||
{
|
||||
ClockDomainId = RequireNonEmpty(clockDomainId, nameof(clockDomainId));
|
||||
SourceId = sourceId;
|
||||
SampleRate = sampleRate;
|
||||
OffsetToVerseTimeNs = offsetToVerseTimeNs;
|
||||
Confidence = confidence;
|
||||
EvidenceKind = evidenceKind;
|
||||
}
|
||||
|
||||
public string ClockDomainId { get; }
|
||||
public string? SourceId { get; }
|
||||
public int SampleRate { get; }
|
||||
public long OffsetToVerseTimeNs { get; }
|
||||
public double Confidence { get; }
|
||||
public string? EvidenceKind { get; }
|
||||
}
|
||||
|
||||
public sealed class CultMeshAudioStreamFormat
|
||||
{
|
||||
public CultMeshAudioStreamFormat(int sampleRate, int channels, string sampleFormat, int framesPerPacket = 0)
|
||||
{
|
||||
if (sampleRate <= 0) throw new ArgumentOutOfRangeException(nameof(sampleRate));
|
||||
if (channels <= 0) throw new ArgumentOutOfRangeException(nameof(channels));
|
||||
SampleRate = sampleRate;
|
||||
Channels = channels;
|
||||
SampleFormat = RequireNonEmpty(sampleFormat, nameof(sampleFormat));
|
||||
FramesPerPacket = framesPerPacket;
|
||||
}
|
||||
|
||||
public int SampleRate { get; }
|
||||
public int Channels { get; }
|
||||
public string SampleFormat { get; }
|
||||
public int FramesPerPacket { get; }
|
||||
}
|
||||
|
||||
public sealed class CultMeshVideoStreamFormat
|
||||
{
|
||||
public CultMeshVideoStreamFormat(int width, int height, string pixelFormat, double framesPerSecond = 0, int planeCount = 1)
|
||||
{
|
||||
if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width));
|
||||
if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height));
|
||||
if (planeCount <= 0) throw new ArgumentOutOfRangeException(nameof(planeCount));
|
||||
Width = width;
|
||||
Height = height;
|
||||
PixelFormat = RequireNonEmpty(pixelFormat, nameof(pixelFormat));
|
||||
FramesPerSecond = framesPerSecond;
|
||||
PlaneCount = planeCount;
|
||||
}
|
||||
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public string PixelFormat { get; }
|
||||
public double FramesPerSecond { get; }
|
||||
public int PlaneCount { get; }
|
||||
}
|
||||
|
||||
public sealed class CultMeshStreamDescriptor
|
||||
{
|
||||
public CultMeshStreamDescriptor(
|
||||
string streamId,
|
||||
string verseId,
|
||||
string ownerPeerId,
|
||||
CultMeshStreamKind kind,
|
||||
CultMeshStreamClock clock,
|
||||
IReadOnlyList<CultMeshStreamBodyTransport> preferredTransports,
|
||||
string? label = null,
|
||||
CultMeshAudioStreamFormat? audio = null,
|
||||
CultMeshVideoStreamFormat? video = null,
|
||||
CultMeshStreamAccess requiredAccess = CultMeshStreamAccess.Read,
|
||||
int maxInFlightFrames = 0,
|
||||
string? metadataSchemaId = null)
|
||||
{
|
||||
StreamId = RequireNonEmpty(streamId, nameof(streamId));
|
||||
VerseId = RequireNonEmpty(verseId, nameof(verseId));
|
||||
OwnerPeerId = RequireNonEmpty(ownerPeerId, nameof(ownerPeerId));
|
||||
Kind = kind;
|
||||
Clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
PreferredTransports = preferredTransports?.ToArray() ?? throw new ArgumentNullException(nameof(preferredTransports));
|
||||
if (PreferredTransports.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one body transport must be preferred.", nameof(preferredTransports));
|
||||
}
|
||||
|
||||
Label = label;
|
||||
Audio = audio;
|
||||
Video = video;
|
||||
RequiredAccess = requiredAccess;
|
||||
MaxInFlightFrames = maxInFlightFrames;
|
||||
MetadataSchemaId = metadataSchemaId;
|
||||
}
|
||||
|
||||
public string StreamId { get; }
|
||||
public string VerseId { get; }
|
||||
public string OwnerPeerId { get; }
|
||||
public CultMeshStreamKind Kind { get; }
|
||||
public string? Label { get; }
|
||||
public CultMeshStreamClock Clock { get; }
|
||||
public CultMeshAudioStreamFormat? Audio { get; }
|
||||
public CultMeshVideoStreamFormat? Video { get; }
|
||||
public IReadOnlyList<CultMeshStreamBodyTransport> PreferredTransports { get; }
|
||||
public CultMeshStreamAccess RequiredAccess { get; }
|
||||
public int MaxInFlightFrames { get; }
|
||||
public string? MetadataSchemaId { get; }
|
||||
}
|
||||
|
||||
public sealed class CultMeshStreamConsumerProfile
|
||||
{
|
||||
public CultMeshStreamConsumerProfile(
|
||||
string peerId,
|
||||
string verseId,
|
||||
IReadOnlyList<CultMeshStreamBodyTransport> supportedTransports,
|
||||
IReadOnlyList<CultMeshStreamKind>? acceptedKinds = null,
|
||||
bool canImportGpuHandles = false,
|
||||
bool canMapSharedMemory = false,
|
||||
int maxInFlightFrames = 0)
|
||||
{
|
||||
PeerId = RequireNonEmpty(peerId, nameof(peerId));
|
||||
VerseId = RequireNonEmpty(verseId, nameof(verseId));
|
||||
SupportedTransports = supportedTransports?.ToArray() ?? throw new ArgumentNullException(nameof(supportedTransports));
|
||||
AcceptedKinds = acceptedKinds?.ToArray() ?? Array.Empty<CultMeshStreamKind>();
|
||||
CanImportGpuHandles = canImportGpuHandles;
|
||||
CanMapSharedMemory = canMapSharedMemory;
|
||||
MaxInFlightFrames = maxInFlightFrames;
|
||||
}
|
||||
|
||||
public string PeerId { get; }
|
||||
public string VerseId { get; }
|
||||
public IReadOnlyList<CultMeshStreamBodyTransport> SupportedTransports { get; }
|
||||
public IReadOnlyList<CultMeshStreamKind> AcceptedKinds { get; }
|
||||
public bool CanImportGpuHandles { get; }
|
||||
public bool CanMapSharedMemory { get; }
|
||||
public int MaxInFlightFrames { get; }
|
||||
}
|
||||
|
||||
public sealed class CultMeshStreamNegotiation
|
||||
{
|
||||
public CultMeshStreamNegotiation(
|
||||
string streamId,
|
||||
string producerPeerId,
|
||||
string consumerPeerId,
|
||||
CultMeshStreamBodyTransport transport,
|
||||
CultMeshStreamAccess access,
|
||||
int maxInFlightFrames,
|
||||
CultMeshStreamCopyBudget copyBudget)
|
||||
{
|
||||
StreamId = streamId;
|
||||
ProducerPeerId = producerPeerId;
|
||||
ConsumerPeerId = consumerPeerId;
|
||||
Transport = transport;
|
||||
Access = access;
|
||||
MaxInFlightFrames = maxInFlightFrames;
|
||||
CopyBudget = copyBudget;
|
||||
}
|
||||
|
||||
public string StreamId { get; }
|
||||
public string ProducerPeerId { get; }
|
||||
public string ConsumerPeerId { get; }
|
||||
public CultMeshStreamBodyTransport Transport { get; }
|
||||
public CultMeshStreamAccess Access { get; }
|
||||
public int MaxInFlightFrames { get; }
|
||||
public CultMeshStreamCopyBudget CopyBudget { get; }
|
||||
}
|
||||
|
||||
public sealed class CultMeshStreamFrameHandle
|
||||
{
|
||||
public CultMeshStreamFrameHandle(
|
||||
string streamId,
|
||||
ulong sequence,
|
||||
long timestampNs,
|
||||
CultMeshStreamBodyTransport transport,
|
||||
long durationNs = 0,
|
||||
int byteLength = 0,
|
||||
string? nativeHandle = null,
|
||||
string? resourceKey = null,
|
||||
string? pageRef = null,
|
||||
string? fenceHandle = null,
|
||||
ulong fenceValue = 0,
|
||||
int unavoidableCopyCount = 0,
|
||||
IReadOnlyDictionary<string, string>? metadata = null)
|
||||
{
|
||||
StreamId = RequireNonEmpty(streamId, nameof(streamId));
|
||||
Sequence = sequence;
|
||||
TimestampNs = timestampNs;
|
||||
DurationNs = durationNs;
|
||||
Transport = transport;
|
||||
ByteLength = byteLength;
|
||||
NativeHandle = nativeHandle;
|
||||
ResourceKey = resourceKey;
|
||||
PageRef = pageRef;
|
||||
FenceHandle = fenceHandle;
|
||||
FenceValue = fenceValue;
|
||||
UnavoidableCopyCount = unavoidableCopyCount;
|
||||
Metadata = metadata ?? new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public string StreamId { get; }
|
||||
public ulong Sequence { get; }
|
||||
public long TimestampNs { get; }
|
||||
public long DurationNs { get; }
|
||||
public CultMeshStreamBodyTransport Transport { get; }
|
||||
public int ByteLength { get; }
|
||||
public string? NativeHandle { get; }
|
||||
public string? ResourceKey { get; }
|
||||
public string? PageRef { get; }
|
||||
public string? FenceHandle { get; }
|
||||
public ulong FenceValue { get; }
|
||||
public int UnavoidableCopyCount { get; }
|
||||
public IReadOnlyDictionary<string, string> Metadata { get; }
|
||||
}
|
||||
|
||||
public readonly struct CultMeshFrameWriteLease
|
||||
{
|
||||
internal CultMeshFrameWriteLease(int slotIndex, ulong sequence, Memory<byte> memory)
|
||||
{
|
||||
SlotIndex = slotIndex;
|
||||
Sequence = sequence;
|
||||
Memory = memory;
|
||||
}
|
||||
|
||||
public int SlotIndex { get; }
|
||||
public ulong Sequence { get; }
|
||||
public Memory<byte> Memory { get; }
|
||||
public Span<byte> Span => Memory.Span;
|
||||
}
|
||||
|
||||
public readonly struct CultMeshFrameReadLease : IDisposable
|
||||
{
|
||||
private readonly CultMeshSharedMemoryFrameRing? _owner;
|
||||
|
||||
internal CultMeshFrameReadLease(
|
||||
CultMeshSharedMemoryFrameRing owner,
|
||||
int slotIndex,
|
||||
CultMeshStreamFrameHandle handle,
|
||||
ReadOnlyMemory<byte> memory)
|
||||
{
|
||||
_owner = owner;
|
||||
SlotIndex = slotIndex;
|
||||
Handle = handle;
|
||||
Memory = memory;
|
||||
}
|
||||
|
||||
public int SlotIndex { get; }
|
||||
public CultMeshStreamFrameHandle Handle { get; }
|
||||
public ReadOnlyMemory<byte> Memory { get; }
|
||||
public ReadOnlySpan<byte> Span => Memory.Span;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_owner?.ReleaseRead(SlotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CultMeshSharedMemoryFrameRingStats
|
||||
{
|
||||
public CultMeshSharedMemoryFrameRingStats(
|
||||
string streamId,
|
||||
int slotCount,
|
||||
int slotByteLength,
|
||||
ulong publishedFrames,
|
||||
ulong droppedFrames,
|
||||
ulong blockedWrites,
|
||||
ulong latestSequence,
|
||||
ulong unavoidableCopyCount)
|
||||
{
|
||||
StreamId = streamId;
|
||||
SlotCount = slotCount;
|
||||
SlotByteLength = slotByteLength;
|
||||
PublishedFrames = publishedFrames;
|
||||
DroppedFrames = droppedFrames;
|
||||
BlockedWrites = blockedWrites;
|
||||
LatestSequence = latestSequence;
|
||||
UnavoidableCopyCount = unavoidableCopyCount;
|
||||
}
|
||||
|
||||
public string StreamId { get; }
|
||||
public int SlotCount { get; }
|
||||
public int SlotByteLength { get; }
|
||||
public ulong PublishedFrames { get; }
|
||||
public ulong DroppedFrames { get; }
|
||||
public ulong BlockedWrites { get; }
|
||||
public ulong LatestSequence { get; }
|
||||
public ulong UnavoidableCopyCount { get; }
|
||||
}
|
||||
|
||||
public sealed class CultMeshSharedMemoryFrameRing : IDisposable
|
||||
{
|
||||
private readonly byte[] _storage;
|
||||
private readonly int[] _readerCounts;
|
||||
private readonly CultMeshStreamFrameHandle?[] _handles;
|
||||
private int _writeCursor;
|
||||
private int _latestSlot = -1;
|
||||
private int _disposed;
|
||||
private ulong _nextSequence;
|
||||
private ulong _publishedFrames;
|
||||
private ulong _droppedFrames;
|
||||
private ulong _blockedWrites;
|
||||
private ulong _unavoidableCopyCount;
|
||||
|
||||
public CultMeshSharedMemoryFrameRing(string streamId, int slotCount, int slotByteLength)
|
||||
{
|
||||
StreamId = RequireNonEmpty(streamId, nameof(streamId));
|
||||
if (slotCount <= 0) throw new ArgumentOutOfRangeException(nameof(slotCount));
|
||||
if (slotByteLength <= 0) throw new ArgumentOutOfRangeException(nameof(slotByteLength));
|
||||
SlotByteLength = slotByteLength;
|
||||
_storage = ArrayPool<byte>.Shared.Rent(checked(slotCount * slotByteLength));
|
||||
_readerCounts = new int[slotCount];
|
||||
_handles = new CultMeshStreamFrameHandle?[slotCount];
|
||||
}
|
||||
|
||||
public string StreamId { get; }
|
||||
public int SlotCount => _handles.Length;
|
||||
public int SlotByteLength { get; }
|
||||
|
||||
public bool TryAcquireWriteSlot(out CultMeshFrameWriteLease lease)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
for (var attempt = 0; attempt < SlotCount; attempt++)
|
||||
{
|
||||
var slotIndex = (_writeCursor + attempt) % SlotCount;
|
||||
if (Volatile.Read(ref _readerCounts[slotIndex]) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_handles[slotIndex] != null)
|
||||
{
|
||||
_droppedFrames++;
|
||||
}
|
||||
|
||||
_writeCursor = (slotIndex + 1) % SlotCount;
|
||||
lease = new CultMeshFrameWriteLease(
|
||||
slotIndex,
|
||||
_nextSequence,
|
||||
new Memory<byte>(_storage, slotIndex * SlotByteLength, SlotByteLength));
|
||||
return true;
|
||||
}
|
||||
|
||||
_blockedWrites++;
|
||||
lease = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public CultMeshStreamFrameHandle CommitWriteSlot(
|
||||
CultMeshFrameWriteLease lease,
|
||||
long timestampNs,
|
||||
int byteLength,
|
||||
long durationNs = 0,
|
||||
int unavoidableCopyCount = 0,
|
||||
IReadOnlyDictionary<string, string>? metadata = null)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
RequireSlot(lease.SlotIndex);
|
||||
if (byteLength < 0 || byteLength > SlotByteLength)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(byteLength));
|
||||
}
|
||||
|
||||
_unavoidableCopyCount += (ulong)Math.Max(0, unavoidableCopyCount);
|
||||
var handle = new CultMeshStreamFrameHandle(
|
||||
StreamId,
|
||||
lease.Sequence,
|
||||
timestampNs,
|
||||
CultMeshStreamBodyTransport.SharedMemory,
|
||||
durationNs,
|
||||
byteLength,
|
||||
resourceKey: $"{StreamId}:slot:{lease.SlotIndex}",
|
||||
unavoidableCopyCount: unavoidableCopyCount,
|
||||
metadata: metadata);
|
||||
_handles[lease.SlotIndex] = handle;
|
||||
_latestSlot = lease.SlotIndex;
|
||||
_nextSequence = lease.Sequence + 1;
|
||||
_publishedFrames++;
|
||||
return handle;
|
||||
}
|
||||
|
||||
public bool TryPublishCopy(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
long timestampNs,
|
||||
long durationNs,
|
||||
out CultMeshStreamFrameHandle handle)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (bytes.Length > SlotByteLength)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bytes));
|
||||
}
|
||||
|
||||
if (!TryAcquireWriteSlot(out var lease))
|
||||
{
|
||||
handle = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes.CopyTo(lease.Span);
|
||||
handle = CommitWriteSlot(lease, timestampNs, bytes.Length, durationNs, unavoidableCopyCount: 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryAcquireLatestRead(out CultMeshFrameReadLease lease)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var slotIndex = Volatile.Read(ref _latestSlot);
|
||||
if (slotIndex < 0)
|
||||
{
|
||||
lease = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var handle = _handles[slotIndex];
|
||||
if (handle == null)
|
||||
{
|
||||
lease = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _readerCounts[slotIndex]);
|
||||
lease = new CultMeshFrameReadLease(
|
||||
this,
|
||||
slotIndex,
|
||||
handle,
|
||||
new ReadOnlyMemory<byte>(_storage, slotIndex * SlotByteLength, handle.ByteLength));
|
||||
return true;
|
||||
}
|
||||
|
||||
public CultMeshSharedMemoryFrameRingStats Stats()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var latest = _latestSlot < 0 ? 0 : _handles[_latestSlot]?.Sequence ?? 0;
|
||||
return new CultMeshSharedMemoryFrameRingStats(
|
||||
StreamId,
|
||||
SlotCount,
|
||||
SlotByteLength,
|
||||
_publishedFrames,
|
||||
_droppedFrames,
|
||||
_blockedWrites,
|
||||
latest,
|
||||
_unavoidableCopyCount);
|
||||
}
|
||||
|
||||
internal void ReleaseRead(int slotIndex)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RequireSlot(slotIndex);
|
||||
Interlocked.Decrement(ref _readerCounts[slotIndex]);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_storage);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(CultMeshSharedMemoryFrameRing));
|
||||
}
|
||||
}
|
||||
|
||||
private void RequireSlot(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= SlotCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(slotIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CultMeshStreamCatalog
|
||||
{
|
||||
private readonly Dictionary<string, CultMeshStreamDescriptor> _streams = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, CultMeshStreamFrameHandle> _latestFrames = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, CultMeshSharedMemoryFrameRing> _rings = new(StringComparer.Ordinal);
|
||||
private readonly object _gate = new();
|
||||
|
||||
public IReadOnlyList<CultMeshStreamDescriptor> Streams
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _streams.Values.OrderBy(stream => stream.StreamId, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Declare(CultMeshStreamDescriptor stream)
|
||||
{
|
||||
if (stream == null) throw new ArgumentNullException(nameof(stream));
|
||||
lock (_gate)
|
||||
{
|
||||
_streams[stream.StreamId] = stream;
|
||||
}
|
||||
}
|
||||
|
||||
public CultMeshStreamDescriptor? Get(string streamId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _streams.TryGetValue(RequireNonEmpty(streamId, nameof(streamId)), out var stream)
|
||||
? stream
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<CultMeshStreamDescriptor> Find(string verseId, CultMeshStreamKind? kind = null)
|
||||
{
|
||||
verseId = RequireNonEmpty(verseId, nameof(verseId));
|
||||
lock (_gate)
|
||||
{
|
||||
return _streams.Values
|
||||
.Where(stream => string.Equals(stream.VerseId, verseId, StringComparison.Ordinal) &&
|
||||
(!kind.HasValue || stream.Kind == kind.Value))
|
||||
.OrderBy(stream => stream.StreamId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public CultMeshStreamNegotiation Negotiate(string streamId, CultMeshStreamConsumerProfile consumer)
|
||||
{
|
||||
if (consumer == null) throw new ArgumentNullException(nameof(consumer));
|
||||
CultMeshStreamDescriptor stream;
|
||||
lock (_gate)
|
||||
{
|
||||
stream = _streams.TryGetValue(RequireNonEmpty(streamId, nameof(streamId)), out var existing)
|
||||
? existing
|
||||
: throw new InvalidOperationException($"Unknown CultMesh stream '{streamId}'.");
|
||||
}
|
||||
if (!string.Equals(stream.VerseId, consumer.VerseId, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("Stream and consumer must belong to the same Verse.");
|
||||
}
|
||||
|
||||
if (consumer.AcceptedKinds.Count > 0 && !consumer.AcceptedKinds.Contains(stream.Kind))
|
||||
{
|
||||
throw new InvalidOperationException($"Consumer does not accept {stream.Kind} streams.");
|
||||
}
|
||||
|
||||
var transport = FirstCompatibleTransport(stream, consumer);
|
||||
if (!transport.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("Stream and consumer have no compatible body transport.");
|
||||
}
|
||||
|
||||
var producerMax = stream.MaxInFlightFrames <= 0 ? int.MaxValue : stream.MaxInFlightFrames;
|
||||
var consumerMax = consumer.MaxInFlightFrames <= 0 ? int.MaxValue : consumer.MaxInFlightFrames;
|
||||
return new CultMeshStreamNegotiation(
|
||||
stream.StreamId,
|
||||
stream.OwnerPeerId,
|
||||
consumer.PeerId,
|
||||
transport.Value,
|
||||
stream.RequiredAccess,
|
||||
Math.Min(producerMax, consumerMax),
|
||||
CopyBudgetFor(transport.Value));
|
||||
}
|
||||
|
||||
public void PublishFrame(CultMeshStreamFrameHandle handle)
|
||||
{
|
||||
if (handle == null) throw new ArgumentNullException(nameof(handle));
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_streams.ContainsKey(handle.StreamId))
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown CultMesh stream '{handle.StreamId}'.");
|
||||
}
|
||||
|
||||
_latestFrames[handle.StreamId] = handle;
|
||||
}
|
||||
}
|
||||
|
||||
public CultMeshStreamFrameHandle? LatestFrame(string streamId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _latestFrames.TryGetValue(RequireNonEmpty(streamId, nameof(streamId)), out var handle)
|
||||
? handle
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public CultMeshSharedMemoryFrameRing CreateSharedMemoryRing(string streamId, int slotCount, int slotByteLength)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_streams.ContainsKey(RequireNonEmpty(streamId, nameof(streamId))))
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown CultMesh stream '{streamId}'.");
|
||||
}
|
||||
|
||||
var ring = new CultMeshSharedMemoryFrameRing(streamId, slotCount, slotByteLength);
|
||||
_rings[streamId] = ring;
|
||||
return ring;
|
||||
}
|
||||
}
|
||||
|
||||
public CultMeshSharedMemoryFrameRing? Ring(string streamId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _rings.TryGetValue(RequireNonEmpty(streamId, nameof(streamId)), out var ring)
|
||||
? ring
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
private static CultMeshStreamCopyBudget CopyBudgetFor(CultMeshStreamBodyTransport transport)
|
||||
{
|
||||
return transport switch
|
||||
{
|
||||
CultMeshStreamBodyTransport.SharedMemory => CultMeshStreamCopyBudget.ZeroCopyTarget,
|
||||
CultMeshStreamBodyTransport.SharedD3D12Texture => CultMeshStreamCopyBudget.ZeroCopyTarget,
|
||||
CultMeshStreamBodyTransport.SharedD3D11Texture => CultMeshStreamCopyBudget.ZeroCopyTarget,
|
||||
CultMeshStreamBodyTransport.DmaBuf => CultMeshStreamCopyBudget.ZeroCopyTarget,
|
||||
CultMeshStreamBodyTransport.IOSurface => CultMeshStreamCopyBudget.ZeroCopyTarget,
|
||||
CultMeshStreamBodyTransport.AHardwareBuffer => CultMeshStreamCopyBudget.ZeroCopyTarget,
|
||||
CultMeshStreamBodyTransport.CultCachePage => CultMeshStreamCopyBudget.OneCopyFallback,
|
||||
_ => CultMeshStreamCopyBudget.OpaqueRuntime
|
||||
};
|
||||
}
|
||||
|
||||
private static CultMeshStreamBodyTransport? FirstCompatibleTransport(
|
||||
CultMeshStreamDescriptor stream,
|
||||
CultMeshStreamConsumerProfile consumer)
|
||||
{
|
||||
foreach (var transport in stream.PreferredTransports)
|
||||
{
|
||||
if (consumer.SupportedTransports.Contains(transport))
|
||||
{
|
||||
return transport;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static partial class CultMeshGuards
|
||||
{
|
||||
internal static string RequireNonEmpty(string value, string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Value must be non-empty.", name);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -203,6 +203,73 @@ to be enough. The replicator can answer that by fetching a shard-bounded
|
|||
snapshot, replacing the local shard view, and advancing its cursor to the
|
||||
snapshot's represented log sequence.
|
||||
|
||||
## Streaming Mode
|
||||
|
||||
CultMesh streaming mode is the low-latency body path for Mimir, Fensalir, Eve,
|
||||
and other realtime runtimes. CultCache and CultNet own typed state, discovery,
|
||||
stream descriptors, negotiations, and frame handles. The frame bodies stay in
|
||||
runtime-native storage: shared D3D12 textures, shared memory rings, platform GPU
|
||||
handles, DMA buffers, or paged CultCache fallbacks when a zero-copy transport is
|
||||
not available.
|
||||
|
||||
```csharp
|
||||
using GameCult.Mesh;
|
||||
|
||||
var streams = CultMesh.CreateStreamCatalog();
|
||||
|
||||
streams.Declare(new CultMeshStreamDescriptor(
|
||||
"mimir:kiyo-pro:rgba",
|
||||
"mimir-live",
|
||||
"starfire",
|
||||
CultMeshStreamKind.Video,
|
||||
new CultMeshStreamClock("mimir:clock", "kiyo-pro", sampleRate: 90_000, confidence: 0.92),
|
||||
new[]
|
||||
{
|
||||
CultMeshStreamBodyTransport.SharedD3D12Texture,
|
||||
CultMeshStreamBodyTransport.SharedMemory,
|
||||
CultMeshStreamBodyTransport.CultCachePage
|
||||
},
|
||||
video: new CultMeshVideoStreamFormat(1920, 1080, "rgba8", framesPerSecond: 60),
|
||||
maxInFlightFrames: 4));
|
||||
|
||||
var fensalir = new CultMeshStreamConsumerProfile(
|
||||
"fensalir",
|
||||
"mimir-live",
|
||||
new[] { CultMeshStreamBodyTransport.SharedD3D12Texture },
|
||||
acceptedKinds: new[] { CultMeshStreamKind.Video },
|
||||
canImportGpuHandles: true);
|
||||
|
||||
var negotiation = streams.Negotiate("mimir:kiyo-pro:rgba", fensalir);
|
||||
```
|
||||
|
||||
For CPU-visible data, `CultMeshSharedMemoryFrameRing` preallocates fixed slots
|
||||
and hands producers writable leases. Consumers hold read leases so the producer
|
||||
does not overwrite a visible frame. `TryPublishCopy` exists for unavoidable
|
||||
fallbacks and increments copy telemetry; the normal hot path writes directly
|
||||
into a lease and commits a `CultMeshStreamFrameHandle`.
|
||||
|
||||
```csharp
|
||||
streams.Declare(new CultMeshStreamDescriptor(
|
||||
"mimir:leap:depth",
|
||||
"mimir-live",
|
||||
"starfire",
|
||||
CultMeshStreamKind.Tensor,
|
||||
new CultMeshStreamClock("mimir:clock", "leap"),
|
||||
new[] { CultMeshStreamBodyTransport.SharedMemory }));
|
||||
|
||||
using var ring = streams.CreateSharedMemoryRing("mimir:leap:depth", slotCount: 4, slotByteLength: 640 * 480 * 2);
|
||||
|
||||
if (ring.TryAcquireWriteSlot(out var write))
|
||||
{
|
||||
FillDepthFrame(write.Span);
|
||||
var frame = ring.CommitWriteSlot(write, timestampNs: GetVerseTimeNs(), byteLength: 640 * 480 * 2);
|
||||
streams.PublishFrame(frame);
|
||||
}
|
||||
```
|
||||
|
||||
The invariant is simple: typed mesh state describes and synchronizes streams;
|
||||
hot frame bodies remain in the cheapest storage both endpoints can share.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Public API](docs/public-api.md)
|
||||
|
|
|
|||
Loading…
Reference in a new issue