From afd8c7156acd595c601ce802c8d2dbeda03692a5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 12 Jul 2026 12:00:59 +0100 Subject: [PATCH] Read provider entity SoA views in Unity --- .../Runtime/EveUnityEntitySoaView.cs | 102 ++++++++++++++++++ .../Runtime/EveUnityEntitySoaView.cs.meta | 11 ++ .../EveUnitySceneSurfaceLowererTests.cs | 40 +++++++ 3 files changed, 153 insertions(+) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs new file mode 100644 index 0000000..2c2253e --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.IO.MemoryMappedFiles; +using System.Linq; +using GameCult.Eve.Surface; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityEntitySoaView : IDisposable + { + private readonly Dictionary _buffers = + new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _columns; + + private EveUnityEntitySoaView(EveEntitySoaViewDocument document) + { + Document = document; + _columns = document.Columns.ToDictionary(column => column.Semantic, StringComparer.Ordinal); + foreach (var buffer in document.Buffers) + { + if (!string.Equals(buffer.Backend, "memory_mapped_file", StringComparison.Ordinal)) + throw new NotSupportedException($"EveUnity does not support entity SoA backend '{buffer.Backend}'."); + var mapped = MemoryMappedFile.OpenExisting(buffer.Location, MemoryMappedFileRights.Read); + _buffers.Add(buffer.BufferId, mapped.CreateViewAccessor( + buffer.ByteOffset, + buffer.ByteLength, + MemoryMappedFileAccess.Read)); + mapped.Dispose(); + } + } + + public EveEntitySoaViewDocument Document { get; } + public long Generation => Document.Generation; + public IReadOnlyList RenderGroups => Document.RenderGroups; + + public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document) + { + if (document == null) throw new ArgumentNullException(nameof(document)); + if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal)) + throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document)); + return new EveUnityEntitySoaView(document); + } + + public bool TryReadVector3(string semantic, int index, out Vector3 value) + { + value = default; + if (!TryColumn(semantic, index, "float32", 12, out var column, out var accessor)) return false; + var offset = column.ByteOffset + (long)index * column.ElementStride; + value = new Vector3(accessor.ReadSingle(offset), accessor.ReadSingle(offset + 4), accessor.ReadSingle(offset + 8)); + return true; + } + + public bool TryReadFloat(string semantic, int index, out float value) + { + value = default; + if (!TryColumn(semantic, index, "float32", 4, out var column, out var accessor)) return false; + value = accessor.ReadSingle(column.ByteOffset + (long)index * column.ElementStride); + return true; + } + + public bool TryReadInt32(string semantic, int index, out int value) + { + value = default; + if (!TryColumn(semantic, index, "int32", 4, out var column, out var accessor)) return false; + value = accessor.ReadInt32(column.ByteOffset + (long)index * column.ElementStride); + return true; + } + + public bool TryReadByte(string semantic, int index, out byte value) + { + value = default; + if (!TryColumn(semantic, index, "uint8", 1, out var column, out var accessor)) return false; + value = accessor.ReadByte(column.ByteOffset + (long)index * column.ElementStride); + return true; + } + + public void Dispose() + { + foreach (var accessor in _buffers.Values) accessor.Dispose(); + _buffers.Clear(); + } + + private bool TryColumn( + string semantic, + int index, + string scalarType, + int minimumStride, + out EveEntitySoaColumn column, + out MemoryMappedViewAccessor accessor) + { + column = null!; + accessor = null!; + if (index < 0 || !_columns.TryGetValue(semantic ?? "", out column)) return false; + if (index >= column.ElementCount || column.ElementStride < minimumStride) return false; + if (!string.Equals(column.ScalarType, scalarType, StringComparison.Ordinal)) return false; + return _buffers.TryGetValue(column.BufferId, out accessor); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs.meta new file mode 100644 index 0000000..8ddec37 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 14693135dfca4740b967148a449cf182 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 7177444..9e133dd 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO.MemoryMappedFiles; using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; @@ -11,6 +12,45 @@ namespace GameCult.Eve.UnityScene.Tests { public sealed class EveUnitySceneSurfaceLowererTests { + [Test] + public void EntitySoaViewReadsGenericSemanticColumnsFromAdvertisedBackend() + { + var location = $"eve-unity-soa-{Guid.NewGuid():N}"; + using var mapped = MemoryMappedFile.CreateNew(location, 32); + using (var writer = mapped.CreateViewAccessor()) + { + writer.Write(0, 4f); + writer.Write(4, 5f); + writer.Write(8, 6f); + writer.Write(12, 7f); + writer.Write(16, 8f); + writer.Write(20, 9f); + writer.Write(24, 41); + writer.Write(28, 42); + } + var document = new EveEntitySoaViewDocument + { + ProviderId = "provider", + ViewId = "pilot", + Generation = 7, + Backend = "memory_mapped_file", + Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", Backend = "memory_mapped_file", Location = location, ByteLength = 32, Generation = 7 } }, + Columns = new[] + { + new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float32", ElementStride = 12, ElementCount = 2 }, + new EveEntitySoaColumn { ColumnId = "entity-index", Semantic = "entity.index", BufferId = "hot", ScalarType = "int32", ByteOffset = 24, ElementStride = 4, ElementCount = 2 } + } + }; + + using var view = EveUnityEntitySoaView.Open(document); + + Assert.That(view.TryReadVector3("transform.position", 1, out var position), Is.True); + Assert.That(position, Is.EqualTo(new Vector3(7f, 8f, 9f))); + Assert.That(view.TryReadInt32("entity.index", 0, out var entityIndex), Is.True); + Assert.That(entityIndex, Is.EqualTo(41)); + Assert.That(view.Generation, Is.EqualTo(7)); + } + [Test] public void LowerCarriesProviderAdvertisedWorldBoundary() {