Consume mapped entity generations without snapshots
This commit is contained in:
parent
0547792516
commit
bedcee3279
3 changed files with 191 additions and 8 deletions
|
|
@ -68,6 +68,8 @@ namespace GameCult.Eve.UnityScene
|
|||
private EveAdvertisedSurface? _advertisedSurface;
|
||||
private CultMeshBodyPublicationResolver? _bodyResolver;
|
||||
private EveEntitySoaViewDocument? _latestEntityLayout;
|
||||
private CultMeshMappedFrameBodyCursor? _mappedEntityFrameCursor;
|
||||
private CultMeshBodyDescriptor? _mappedEntityFrameContract;
|
||||
private long _lastQueuedEntityViewEpoch = -1;
|
||||
private long _lastQueuedEntityViewSequence = -1;
|
||||
private bool _disposed;
|
||||
|
|
@ -186,6 +188,8 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public void PumpLiveEvents()
|
||||
{
|
||||
ThrowIfSubscriptionFailed(_entitySubscriptions);
|
||||
ThrowIfSubscriptionFailed(_subscriptions);
|
||||
while (_liveDocuments.TryDequeue(out var document))
|
||||
{
|
||||
if (document is EveEntitySoaViewDocument entityView)
|
||||
|
|
@ -197,10 +201,21 @@ namespace GameCult.Eve.UnityScene
|
|||
else if (document is EveCommandReceiptDocument receipt)
|
||||
PublishReceipt(receipt);
|
||||
}
|
||||
PumpMappedEntityFrame();
|
||||
}
|
||||
|
||||
private static void ThrowIfSubscriptionFailed(CultNetDatabaseSubscriptionClient? subscription)
|
||||
{
|
||||
var failure = subscription?.BackgroundFailure;
|
||||
if (failure?.IsCompletedSuccessfully == true)
|
||||
throw new InvalidOperationException(
|
||||
"The retained CultMesh state subscription failed.",
|
||||
failure.Result);
|
||||
}
|
||||
|
||||
private void QueueEntityView(EveEntitySoaViewDocument document)
|
||||
{
|
||||
TraceHotState($"layout epoch={document.ProducerEpoch} sequence={document.Sequence} identities={document.Identities?.Length ?? 0}");
|
||||
_latestEntityLayout = document;
|
||||
if (document.ProducerEpoch < _lastQueuedEntityViewEpoch ||
|
||||
(document.ProducerEpoch == _lastQueuedEntityViewEpoch &&
|
||||
|
|
@ -216,9 +231,25 @@ namespace GameCult.Eve.UnityScene
|
|||
var layout = _latestEntityLayout;
|
||||
if (layout == null || layout.Buffers == null || layout.Buffers.Length == 0 ||
|
||||
!string.Equals(layout.Buffers[0].BufferId, publication.BodyId, StringComparison.Ordinal))
|
||||
{
|
||||
TraceHotState($"publication skipped body={publication.BodyId} sequence={publication.Sequence} layout={(layout == null ? "missing" : "incompatible")}");
|
||||
return;
|
||||
}
|
||||
|
||||
QueueEntityView(new EveEntitySoaViewDocument
|
||||
TraceHotState($"publication body={publication.BodyId} epoch={publication.ProducerEpoch} sequence={publication.Sequence} plane={string.Join(",", publication.Representations.Select(value => value.TransportKind))}");
|
||||
|
||||
QueueEntityView(MaterializeEntityGeneration(
|
||||
layout,
|
||||
publication.ProducerEpoch,
|
||||
publication.Sequence));
|
||||
}
|
||||
|
||||
private static EveEntitySoaViewDocument MaterializeEntityGeneration(
|
||||
EveEntitySoaViewDocument layout,
|
||||
long producerEpoch,
|
||||
long sequence)
|
||||
{
|
||||
return new EveEntitySoaViewDocument
|
||||
{
|
||||
Schema = layout.Schema,
|
||||
ProviderId = layout.ProviderId,
|
||||
|
|
@ -226,8 +257,8 @@ namespace GameCult.Eve.UnityScene
|
|||
PublishedAtUtc = DateTimeOffset.UtcNow.ToString("O"),
|
||||
BodySchemaId = layout.BodySchemaId,
|
||||
LayoutVersion = layout.LayoutVersion,
|
||||
ProducerEpoch = publication.ProducerEpoch,
|
||||
Sequence = publication.Sequence,
|
||||
ProducerEpoch = producerEpoch,
|
||||
Sequence = sequence,
|
||||
Capacity = layout.Capacity,
|
||||
Buffers = layout.Buffers,
|
||||
Columns = layout.Columns,
|
||||
|
|
@ -237,13 +268,13 @@ namespace GameCult.Eve.UnityScene
|
|||
ColumnId = range.ColumnId,
|
||||
StartIndex = range.StartIndex,
|
||||
Count = range.Count,
|
||||
Sequence = publication.Sequence
|
||||
Sequence = sequence
|
||||
})
|
||||
.ToArray(),
|
||||
RenderGroups = layout.RenderGroups,
|
||||
Identities = layout.Identities,
|
||||
FrameId = publication.Sequence
|
||||
});
|
||||
FrameId = sequence
|
||||
};
|
||||
}
|
||||
|
||||
public void SubmitCommand(EveSurfaceCommandRequest request)
|
||||
|
|
@ -291,6 +322,9 @@ namespace GameCult.Eve.UnityScene
|
|||
_prefabs.Clear();
|
||||
_nativeAssets.Clear();
|
||||
_nativeAssetMetadata.Clear();
|
||||
_mappedEntityFrameCursor?.Dispose();
|
||||
_mappedEntityFrameCursor = null;
|
||||
_mappedEntityFrameContract = null;
|
||||
_subscriptions?.Dispose();
|
||||
_entitySubscriptions?.Dispose();
|
||||
_node?.Dispose();
|
||||
|
|
@ -513,6 +547,7 @@ namespace GameCult.Eve.UnityScene
|
|||
var now = DateTimeOffset.UtcNow;
|
||||
if (!IsPublicationLive(publication, now))
|
||||
return;
|
||||
EnsureMappedEntityFrameCursor(publication);
|
||||
_bodyResolver ??= CreateBodyResolver();
|
||||
var lease = _bodyResolver.ResolveReadOnly(publication, new CultMeshBodyValidationRequest
|
||||
{
|
||||
|
|
@ -542,6 +577,82 @@ namespace GameCult.Eve.UnityScene
|
|||
}
|
||||
}
|
||||
|
||||
private void EnsureMappedEntityFrameCursor(CultMeshBodyPublicationDocument publication)
|
||||
{
|
||||
var descriptor = (publication.Representations ?? Array.Empty<CultMeshBodyDescriptor>())
|
||||
.FirstOrDefault(CultMeshMappedFrameBodyCursor.CanOpen);
|
||||
if (descriptor == null)
|
||||
{
|
||||
_mappedEntityFrameCursor?.Dispose();
|
||||
_mappedEntityFrameCursor = null;
|
||||
_mappedEntityFrameContract = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var current = _mappedEntityFrameContract;
|
||||
if (current != null &&
|
||||
string.Equals(FrameCapabilityIdentity(current.CapabilityToken),
|
||||
FrameCapabilityIdentity(descriptor.CapabilityToken), StringComparison.Ordinal) &&
|
||||
string.Equals(current.BodyId, descriptor.BodyId, StringComparison.Ordinal) &&
|
||||
string.Equals(current.SchemaId, descriptor.SchemaId, StringComparison.Ordinal) &&
|
||||
current.LayoutVersion == descriptor.LayoutVersion &&
|
||||
current.Capacity == descriptor.Capacity &&
|
||||
current.ProducerEpoch == descriptor.ProducerEpoch &&
|
||||
current.ByteSize == descriptor.ByteSize)
|
||||
return;
|
||||
|
||||
_mappedEntityFrameCursor?.Dispose();
|
||||
_mappedEntityFrameCursor = new CultMeshMappedFrameBodyCursor(descriptor);
|
||||
_mappedEntityFrameContract = descriptor;
|
||||
TraceHotState(
|
||||
$"mapped frame cursor body={descriptor.BodyId} epoch={descriptor.ProducerEpoch} sequence={descriptor.Sequence}");
|
||||
}
|
||||
|
||||
private void PumpMappedEntityFrame()
|
||||
{
|
||||
var cursor = _mappedEntityFrameCursor;
|
||||
var layout = _latestEntityLayout;
|
||||
if (cursor == null || layout == null || !cursor.TryAcquireLatest(out var lease))
|
||||
return;
|
||||
|
||||
var descriptor = lease.Descriptor;
|
||||
if (descriptor.ProducerEpoch < _lastQueuedEntityViewEpoch ||
|
||||
(descriptor.ProducerEpoch == _lastQueuedEntityViewEpoch &&
|
||||
descriptor.Sequence <= _lastQueuedEntityViewSequence))
|
||||
{
|
||||
lease.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
_lastQueuedEntityViewEpoch = descriptor.ProducerEpoch;
|
||||
_lastQueuedEntityViewSequence = descriptor.Sequence;
|
||||
var generation = MaterializeEntityGeneration(
|
||||
layout,
|
||||
descriptor.ProducerEpoch,
|
||||
descriptor.Sequence);
|
||||
var handler = EntityViewAvailable;
|
||||
if (handler == null)
|
||||
{
|
||||
lease.Dispose();
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
handler(generation, lease);
|
||||
}
|
||||
catch
|
||||
{
|
||||
lease.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FrameCapabilityIdentity(string capabilityToken)
|
||||
{
|
||||
var separator = capabilityToken.LastIndexOf('.');
|
||||
return separator > 0 ? capabilityToken.Substring(0, separator) : capabilityToken;
|
||||
}
|
||||
|
||||
private static bool IsPublicationLive(
|
||||
CultMeshBodyPublicationDocument publication,
|
||||
DateTimeOffset now) =>
|
||||
|
|
@ -645,6 +756,7 @@ namespace GameCult.Eve.UnityScene
|
|||
CultMeshBodyTransportKind.Network.ToString()
|
||||
})
|
||||
.GetAwaiter().GetResult();
|
||||
TraceHotState($"initial subscription documents=[{string.Join(",", initial.Select(value => value.GetType().Name))}]");
|
||||
foreach (var document in initial)
|
||||
{
|
||||
if (document is EveEntitySoaViewDocument current)
|
||||
|
|
@ -686,6 +798,7 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
private void OnReplicatedDocumentChanged(CultNetReplicatedDocumentChange change)
|
||||
{
|
||||
TraceHotState($"change subscription={change.SubscriptionId} kind={change.ChangeKind} document={change.Document?.GetType().Name ?? "null"}");
|
||||
if (change.Document is EveEntitySoaViewDocument entityView)
|
||||
QueueEntityView(entityView);
|
||||
else if (change.Document is CultMeshBodyPublicationDocument publication)
|
||||
|
|
@ -694,6 +807,12 @@ namespace GameCult.Eve.UnityScene
|
|||
_liveDocuments.Enqueue(change.Document);
|
||||
}
|
||||
|
||||
private static void TraceHotState(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_RUDP"), "1", StringComparison.Ordinal))
|
||||
Debug.Log($"EveUnity CultMesh hot state: {message}");
|
||||
}
|
||||
|
||||
private void RefreshAssetCatalog()
|
||||
{
|
||||
var interaction = RequireWorldInteraction();
|
||||
|
|
|
|||
|
|
@ -87,6 +87,70 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
Assert.That(generation.DirtyRanges.All(range => range.Sequence == publication.Sequence), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LiveTransportReadsLaterMappedFramesWithoutNewControlDocuments()
|
||||
{
|
||||
using var transport = new EveUnityCultMeshLiveProviderTransport(
|
||||
"test-replica.cc",
|
||||
"cultnet://127.0.0.1:3075",
|
||||
"provider",
|
||||
"provider.pilot");
|
||||
var view = EntityLeaseDocument();
|
||||
view.Sequence = 0;
|
||||
using var publisher = new CultMeshFrameBodyPublisher(
|
||||
view.Buffers[0].BufferId,
|
||||
view.BodySchemaId,
|
||||
view.LayoutVersion,
|
||||
view.Capacity,
|
||||
view.ProducerEpoch,
|
||||
slotByteLength: 4);
|
||||
publisher.TryPublish(new byte[] { 1, 2, 3, 4 }, DateTimeOffset.UtcNow, out var bootstrap);
|
||||
var publication = new CultMeshBodyPublicationDocument
|
||||
{
|
||||
BodyId = bootstrap.BodyId,
|
||||
ProducerId = view.ProviderId,
|
||||
SchemaId = bootstrap.SchemaId,
|
||||
LayoutVersion = bootstrap.LayoutVersion,
|
||||
ByteSize = bootstrap.ByteSize,
|
||||
Capacity = bootstrap.Capacity,
|
||||
ProducerEpoch = bootstrap.ProducerEpoch,
|
||||
Sequence = bootstrap.Sequence,
|
||||
Synchronization = bootstrap.Synchronization,
|
||||
LivenessExpiresAtUnixMs = bootstrap.LeaseExpiresAtUnixMs,
|
||||
Representations = new[] { bootstrap }
|
||||
};
|
||||
var queueView = typeof(EveUnityCultMeshLiveProviderTransport)
|
||||
.GetMethod("QueueEntityView", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var ensureCursor = typeof(EveUnityCultMeshLiveProviderTransport)
|
||||
.GetMethod("EnsureMappedEntityFrameCursor", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var pumpCursor = typeof(EveUnityCultMeshLiveProviderTransport)
|
||||
.GetMethod("PumpMappedEntityFrame", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var queue = (System.Collections.Concurrent.ConcurrentQueue<object>)
|
||||
typeof(EveUnityCultMeshLiveProviderTransport)
|
||||
.GetField("_liveDocuments", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.GetValue(transport)!;
|
||||
queueView.Invoke(transport, new object[] { view });
|
||||
queue.TryDequeue(out _);
|
||||
ensureCursor.Invoke(transport, new object[] { publication });
|
||||
|
||||
publisher.TryPublish(new byte[] { 9, 8, 7, 6 }, DateTimeOffset.UtcNow, out _);
|
||||
EveEntitySoaViewDocument? presented = null;
|
||||
byte presentedByte = 0;
|
||||
transport.EntityViewAvailable += (generation, lease) =>
|
||||
{
|
||||
presented = generation;
|
||||
presentedByte = lease.ReadByte(0);
|
||||
lease.Dispose();
|
||||
};
|
||||
|
||||
pumpCursor.Invoke(transport, Array.Empty<object>());
|
||||
|
||||
Assert.That(presented, Is.Not.Null);
|
||||
Assert.That(presented!.Sequence, Is.EqualTo(1));
|
||||
Assert.That(presentedByte, Is.EqualTo(9));
|
||||
Assert.That(queue, Is.Empty, "mapped frames must not be represented as queued CultNet documents");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LiveTransportDropsExpiredHistoricalGenerationWithoutReinterpretingLatest()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.3.76",
|
||||
"version": "0.3.77",
|
||||
"unity": "6000.0",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
"org.gamecult.cultlib": "1.0.21",
|
||||
"org.gamecult.cultlib": "1.0.22",
|
||||
"org.gamecult.eve.plugin-fields": "0.2.3",
|
||||
"org.gamecult.eve.surface": "0.2.4",
|
||||
"com.unity.mathematics": "1.3.2",
|
||||
|
|
|
|||
Reference in a new issue