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 _buffersById; private readonly Dictionary _columnsById; private readonly Dictionary> _columnsByKind; private readonly Dictionary> _dirtyRangesByColumnId; private readonly Dictionary _renderGroupsById; private AetheriaRuntimeDaemonSoaViewIndex( AetheriaRuntimeDaemonSoaViewDocument view, Dictionary buffersById, Dictionary columnsById, Dictionary> columnsByKind, Dictionary> dirtyRangesByColumnId, Dictionary 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 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(); var buffersById = new Dictionary(StringComparer.Ordinal); var columnsById = new Dictionary(StringComparer.Ordinal); var columnsByKind = new Dictionary>(StringComparer.Ordinal); var dirtyRangesByColumnId = new Dictionary>(StringComparer.Ordinal); var renderGroupsById = new Dictionary(); if (!string.Equals(effectiveView.Schema, AetheriaRuntimeDaemonSchemas.SoaView, StringComparison.Ordinal)) { errors.Add($"Unexpected SoA schema '{effectiveView.Schema}'."); } foreach (var buffer in effectiveView.Buffers ?? Array.Empty()) { 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()) { 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(); columnsByKind[column.Kind ?? ""] = kindBindings; } kindBindings.Add(binding); } foreach (var dirtyRange in effectiveView.DirtyRanges ?? Array.Empty()) { 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(); dirtyRangesByColumnId[dirtyRange.ColumnId ?? ""] = ranges; } ranges.Add(dirtyRange); } foreach (var renderGroup in effectiveView.RenderGroups ?? Array.Empty()) { 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 GetColumnsOfKind(string kind) { return _columnsByKind.TryGetValue(kind ?? "", out var bindings) ? bindings : Array.Empty(); } public IReadOnlyList GetDirtyRanges(string columnId) { return _dirtyRangesByColumnId.TryGetValue(columnId ?? "", out var ranges) ? ranges : Array.Empty(); } public IReadOnlyList RenderGroups => View.RenderGroups ?? Array.Empty(); 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 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; } } }