Aetheria/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaViewIndex.cs
2026-06-25 18:09:33 +01:00

462 lines
19 KiB
C#

using System;
using System.Collections.Generic;
#nullable enable
namespace GameCult.Aetheria.State.Verse
{
public sealed class AetheriaRuntimeDaemonSoaColumnBinding
{
public AetheriaRuntimeDaemonSoaColumnBinding(
AetheriaRuntimeDaemonSoaColumnDocument column,
AetheriaRuntimeDaemonSoaBufferDocument buffer,
long absoluteByteOffset,
long byteLength,
bool directMemoryCompatible)
{
Column = column ?? throw new ArgumentNullException(nameof(column));
Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
AbsoluteByteOffset = absoluteByteOffset;
ByteLength = byteLength;
DirectMemoryCompatible = directMemoryCompatible;
}
public AetheriaRuntimeDaemonSoaColumnDocument Column { get; }
public AetheriaRuntimeDaemonSoaBufferDocument Buffer { get; }
public long AbsoluteByteOffset { get; }
public long ByteLength { get; }
public bool DirectMemoryCompatible { get; }
}
public sealed class AetheriaRuntimeDaemonSoaViewIndex
{
private readonly Dictionary<string, AetheriaRuntimeDaemonSoaBufferDocument> _buffersById;
private readonly Dictionary<string, AetheriaRuntimeDaemonSoaColumnBinding> _columnsById;
private readonly Dictionary<string, List<AetheriaRuntimeDaemonSoaColumnBinding>> _columnsByKind;
private readonly Dictionary<string, List<AetheriaRuntimeDaemonSoaDirtyRangeDocument>> _dirtyRangesByColumnId;
private readonly Dictionary<int, AetheriaRuntimeDaemonRenderGroupDocument> _renderGroupsById;
private AetheriaRuntimeDaemonSoaViewIndex(
AetheriaRuntimeDaemonSoaViewDocument view,
Dictionary<string, AetheriaRuntimeDaemonSoaBufferDocument> buffersById,
Dictionary<string, AetheriaRuntimeDaemonSoaColumnBinding> columnsById,
Dictionary<string, List<AetheriaRuntimeDaemonSoaColumnBinding>> columnsByKind,
Dictionary<string, List<AetheriaRuntimeDaemonSoaDirtyRangeDocument>> dirtyRangesByColumnId,
Dictionary<int, AetheriaRuntimeDaemonRenderGroupDocument> renderGroupsById,
string[] validationErrors)
{
View = view ?? throw new ArgumentNullException(nameof(view));
_buffersById = buffersById;
_columnsById = columnsById;
_columnsByKind = columnsByKind;
_dirtyRangesByColumnId = dirtyRangesByColumnId;
_renderGroupsById = renderGroupsById;
ValidationErrors = validationErrors;
}
public AetheriaRuntimeDaemonSoaViewDocument View { get; }
public IReadOnlyList<string> ValidationErrors { get; }
public bool IsValid => ValidationErrors.Count == 0;
public static AetheriaRuntimeDaemonSoaViewIndex Empty { get; } =
Build(new AetheriaRuntimeDaemonSoaViewDocument { IsAuthoritative = false });
public static AetheriaRuntimeDaemonSoaViewIndex Build(
AetheriaRuntimeDaemonSoaViewDocument? view,
bool requireObserverReadOnly = true)
{
var effectiveView = view ?? new AetheriaRuntimeDaemonSoaViewDocument { IsAuthoritative = false };
var errors = new List<string>();
var buffersById = new Dictionary<string, AetheriaRuntimeDaemonSoaBufferDocument>(StringComparer.Ordinal);
var columnsById = new Dictionary<string, AetheriaRuntimeDaemonSoaColumnBinding>(StringComparer.Ordinal);
var columnsByKind = new Dictionary<string, List<AetheriaRuntimeDaemonSoaColumnBinding>>(StringComparer.Ordinal);
var dirtyRangesByColumnId = new Dictionary<string, List<AetheriaRuntimeDaemonSoaDirtyRangeDocument>>(StringComparer.Ordinal);
var renderGroupsById = new Dictionary<int, AetheriaRuntimeDaemonRenderGroupDocument>();
if (!string.Equals(effectiveView.Schema, AetheriaRuntimeDaemonSchemas.SoaView, StringComparison.Ordinal))
{
errors.Add($"Unexpected SoA schema '{effectiveView.Schema}'.");
}
foreach (var buffer in effectiveView.Buffers ?? Array.Empty<AetheriaRuntimeDaemonSoaBufferDocument>())
{
if (string.IsNullOrWhiteSpace(buffer.BufferId))
{
errors.Add("SoA buffer is missing a buffer id.");
continue;
}
if (buffersById.ContainsKey(buffer.BufferId))
{
errors.Add($"Duplicate SoA buffer id '{buffer.BufferId}'.");
continue;
}
if (buffer.ByteOffset < 0)
{
errors.Add($"SoA buffer '{buffer.BufferId}' has a negative byte offset.");
}
if (buffer.ByteLength < 0)
{
errors.Add($"SoA buffer '{buffer.BufferId}' has a negative byte length.");
}
if (requireObserverReadOnly && buffer.ObserverWritable)
{
errors.Add($"SoA buffer '{buffer.BufferId}' is observer-writable; clients must observe daemon state read-only.");
}
buffersById.Add(buffer.BufferId, buffer);
}
foreach (var column in effectiveView.Columns ?? Array.Empty<AetheriaRuntimeDaemonSoaColumnDocument>())
{
if (string.IsNullOrWhiteSpace(column.ColumnId))
{
errors.Add("SoA column is missing a column id.");
continue;
}
if (columnsById.ContainsKey(column.ColumnId))
{
errors.Add($"Duplicate SoA column id '{column.ColumnId}'.");
continue;
}
if (!buffersById.TryGetValue(column.BufferId ?? "", out var buffer))
{
errors.Add($"SoA column '{column.ColumnId}' references missing buffer '{column.BufferId}'.");
continue;
}
if (string.IsNullOrWhiteSpace(column.Kind))
{
errors.Add($"SoA column '{column.ColumnId}' is missing a semantic kind.");
}
if (column.ByteOffset < 0)
{
errors.Add($"SoA column '{column.ColumnId}' has a negative byte offset.");
}
if (column.ElementStride <= 0)
{
errors.Add($"SoA column '{column.ColumnId}' has a non-positive element stride.");
}
if (column.ElementCount < 0)
{
errors.Add($"SoA column '{column.ColumnId}' has a negative element count.");
}
var scalarByteLength = GetScalarByteLength(column.ScalarType);
if (scalarByteLength <= 0)
{
errors.Add($"SoA column '{column.ColumnId}' has unsupported scalar type '{column.ScalarType}'.");
scalarByteLength = Math.Max(1, column.ElementStride);
}
var columnByteLength = GetColumnByteLength(column.ElementCount, column.ElementStride, scalarByteLength);
var absoluteByteOffset = buffer.ByteOffset + column.ByteOffset;
if (column.ByteOffset >= 0 && columnByteLength >= 0 && buffer.ByteLength >= 0)
{
var columnEnd = column.ByteOffset + columnByteLength;
if (columnEnd > buffer.ByteLength)
{
errors.Add($"SoA column '{column.ColumnId}' exceeds buffer '{buffer.BufferId}'.");
}
}
var binding = new AetheriaRuntimeDaemonSoaColumnBinding(
column,
buffer,
absoluteByteOffset,
columnByteLength,
IsDirectMemoryCompatible(buffer));
ValidateSemanticColumn(column, errors);
columnsById.Add(column.ColumnId, binding);
if (!columnsByKind.TryGetValue(column.Kind ?? "", out var kindBindings))
{
kindBindings = new List<AetheriaRuntimeDaemonSoaColumnBinding>();
columnsByKind[column.Kind ?? ""] = kindBindings;
}
kindBindings.Add(binding);
}
foreach (var dirtyRange in effectiveView.DirtyRanges ?? Array.Empty<AetheriaRuntimeDaemonSoaDirtyRangeDocument>())
{
if (!columnsById.TryGetValue(dirtyRange.ColumnId ?? "", out var binding))
{
errors.Add($"SoA dirty range references missing column '{dirtyRange.ColumnId}'.");
continue;
}
if (dirtyRange.StartIndex < 0)
{
errors.Add($"SoA dirty range for column '{dirtyRange.ColumnId}' has a negative start index.");
}
if (dirtyRange.Count < 0)
{
errors.Add($"SoA dirty range for column '{dirtyRange.ColumnId}' has a negative count.");
}
if (dirtyRange.StartIndex >= 0 && dirtyRange.Count >= 0 &&
dirtyRange.StartIndex + dirtyRange.Count > binding.Column.ElementCount)
{
errors.Add($"SoA dirty range for column '{dirtyRange.ColumnId}' exceeds the column element count.");
}
if (!dirtyRangesByColumnId.TryGetValue(dirtyRange.ColumnId ?? "", out var ranges))
{
ranges = new List<AetheriaRuntimeDaemonSoaDirtyRangeDocument>();
dirtyRangesByColumnId[dirtyRange.ColumnId ?? ""] = ranges;
}
ranges.Add(dirtyRange);
}
foreach (var renderGroup in effectiveView.RenderGroups ?? Array.Empty<AetheriaRuntimeDaemonRenderGroupDocument>())
{
if (renderGroup.GroupId < 0)
{
errors.Add($"Render group '{renderGroup.GroupId}' has a negative group id.");
continue;
}
if (renderGroupsById.ContainsKey(renderGroup.GroupId))
{
errors.Add($"Duplicate render group id '{renderGroup.GroupId}'.");
continue;
}
if (string.IsNullOrWhiteSpace(renderGroup.MeshKey))
{
errors.Add($"Render group '{renderGroup.GroupId}' is missing a mesh key.");
}
if (string.IsNullOrWhiteSpace(renderGroup.MaterialKey))
{
errors.Add($"Render group '{renderGroup.GroupId}' is missing a material key.");
}
if (renderGroup.SubMeshIndex < 0)
{
errors.Add($"Render group '{renderGroup.GroupId}' has a negative submesh index.");
}
if (renderGroup.InstanceCount < -1)
{
errors.Add($"Render group '{renderGroup.GroupId}' has an invalid instance count.");
}
if (renderGroup.BoundsSizeX <= 0 ||
renderGroup.BoundsSizeY <= 0 ||
renderGroup.BoundsSizeZ <= 0)
{
errors.Add($"Render group '{renderGroup.GroupId}' must publish positive render bounds.");
}
if (!IsValidShadowMode(renderGroup.ShadowMode))
{
errors.Add($"Render group '{renderGroup.GroupId}' has invalid shadow mode '{renderGroup.ShadowMode}'.");
}
if (renderGroup.DefaultScale <= 0)
{
errors.Add($"Render group '{renderGroup.GroupId}' has a non-positive default scale.");
}
if (renderGroup.Lod < -1)
{
errors.Add($"Render group '{renderGroup.GroupId}' has an invalid render lod.");
}
renderGroupsById.Add(renderGroup.GroupId, renderGroup);
}
if (renderGroupsById.Count > 1 &&
!columnsByKind.ContainsKey(AetheriaRuntimeDaemonSoaColumnKinds.RenderGroupId))
{
errors.Add("Multiple render groups require a render group id column.");
}
return new AetheriaRuntimeDaemonSoaViewIndex(
effectiveView,
buffersById,
columnsById,
columnsByKind,
dirtyRangesByColumnId,
renderGroupsById,
errors.ToArray());
}
public bool TryGetBuffer(string bufferId, out AetheriaRuntimeDaemonSoaBufferDocument buffer)
{
return _buffersById.TryGetValue(bufferId ?? "", out buffer);
}
public bool TryGetColumn(string columnId, out AetheriaRuntimeDaemonSoaColumnBinding binding)
{
return _columnsById.TryGetValue(columnId ?? "", out binding);
}
public bool TryGetFirstColumnOfKind(string kind, out AetheriaRuntimeDaemonSoaColumnBinding binding)
{
if (_columnsByKind.TryGetValue(kind ?? "", out var bindings) && bindings.Count > 0)
{
binding = bindings[0];
return true;
}
binding = null!;
return false;
}
public IReadOnlyList<AetheriaRuntimeDaemonSoaColumnBinding> GetColumnsOfKind(string kind)
{
return _columnsByKind.TryGetValue(kind ?? "", out var bindings)
? bindings
: Array.Empty<AetheriaRuntimeDaemonSoaColumnBinding>();
}
public IReadOnlyList<AetheriaRuntimeDaemonSoaDirtyRangeDocument> GetDirtyRanges(string columnId)
{
return _dirtyRangesByColumnId.TryGetValue(columnId ?? "", out var ranges)
? ranges
: Array.Empty<AetheriaRuntimeDaemonSoaDirtyRangeDocument>();
}
public IReadOnlyList<AetheriaRuntimeDaemonRenderGroupDocument> RenderGroups =>
View.RenderGroups ?? Array.Empty<AetheriaRuntimeDaemonRenderGroupDocument>();
public bool TryGetRenderGroup(int groupId, out AetheriaRuntimeDaemonRenderGroupDocument renderGroup)
{
return _renderGroupsById.TryGetValue(groupId, out renderGroup);
}
private static bool IsDirectMemoryCompatible(AetheriaRuntimeDaemonSoaBufferDocument buffer)
{
return !buffer.ObserverWritable &&
(string.Equals(buffer.Backend, AetheriaRuntimeDaemonSoaBackends.MemoryMappedFile, StringComparison.Ordinal) ||
string.Equals(buffer.Backend, AetheriaRuntimeDaemonSoaBackends.SharedNativeMemory, StringComparison.Ordinal));
}
private static void ValidateSemanticColumn(
AetheriaRuntimeDaemonSoaColumnDocument column,
List<string> errors)
{
switch (column.Kind)
{
case AetheriaRuntimeDaemonSoaColumnKinds.EntityIndex:
if (!string.Equals(column.ScalarType, "int32", StringComparison.Ordinal))
{
errors.Add($"SoA column '{column.ColumnId}' entity index must use int32.");
}
break;
case AetheriaRuntimeDaemonSoaColumnKinds.Position:
case AetheriaRuntimeDaemonSoaColumnKinds.Velocity:
if (!string.Equals(column.ScalarType, "float3", StringComparison.Ordinal))
{
errors.Add($"SoA column '{column.ColumnId}' {column.Kind} must use float3.");
}
break;
case AetheriaRuntimeDaemonSoaColumnKinds.RotationRadians:
case AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyRadius:
case AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyMass:
case AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyInverseMass:
if (!string.Equals(column.ScalarType, "float32", StringComparison.Ordinal))
{
errors.Add($"SoA column '{column.ColumnId}' {column.Kind} must use float32.");
}
break;
case AetheriaRuntimeDaemonSoaColumnKinds.RenderScale:
if (!string.Equals(column.ScalarType, "float32", StringComparison.Ordinal))
{
errors.Add($"SoA column '{column.ColumnId}' render scale must use float32.");
}
break;
case AetheriaRuntimeDaemonSoaColumnKinds.RenderVisibility:
if (!string.Equals(column.ScalarType, "bool", StringComparison.Ordinal) &&
!string.Equals(column.ScalarType, "byte", StringComparison.Ordinal) &&
!string.Equals(column.ScalarType, "uint8", StringComparison.Ordinal))
{
errors.Add($"SoA column '{column.ColumnId}' render visibility must use bool, byte, or uint8.");
}
break;
case AetheriaRuntimeDaemonSoaColumnKinds.RenderLod:
if (!string.Equals(column.ScalarType, "int32", StringComparison.Ordinal))
{
errors.Add($"SoA column '{column.ColumnId}' render lod must use int32.");
}
break;
}
}
private static bool IsValidShadowMode(string? shadowMode)
{
switch (shadowMode)
{
case AetheriaRuntimeDaemonRenderShadowModes.Off:
case AetheriaRuntimeDaemonRenderShadowModes.On:
case AetheriaRuntimeDaemonRenderShadowModes.TwoSided:
case AetheriaRuntimeDaemonRenderShadowModes.ShadowsOnly:
return true;
default:
return false;
}
}
private static int GetScalarByteLength(string? scalarType)
{
switch (scalarType)
{
case "float3":
return 12;
case "float32":
case "int32":
case "uint32":
return 4;
case "float64":
case "int64":
case "uint64":
return 8;
case "int16":
case "uint16":
return 2;
case "byte":
case "uint8":
case "int8":
case "bool":
return 1;
default:
return 0;
}
}
private static long GetColumnByteLength(int elementCount, int elementStride, int scalarByteLength)
{
if (elementCount <= 0)
{
return 0;
}
if (elementStride <= 0 || scalarByteLength <= 0)
{
return -1;
}
return ((long)elementCount - 1) * elementStride + scalarByteLength;
}
}
}