using Chorizite.Core.Lib; using Chorizite.Core.Render.Enums; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; using BoundingBox = Chorizite.Core.Lib.BoundingBox; namespace AcDream.Content; /// /// Vertex format for scenery mesh rendering: position, normal, UV. /// [StructLayout(LayoutKind.Sequential)] public struct VertexPositionNormalTexture { public Vector3 Position; public Vector3 Normal; public Vector2 UV; public static int Size => 8 * sizeof(float); // 3+3+2 = 8 floats = 32 bytes public VertexPositionNormalTexture(Vector3 position, Vector3 normal, Vector2 uv) { Position = position; Normal = normal; UV = uv; } } /// /// Staged data for a particle emitter to be created on the GL thread. /// public struct StagedEmitter { public ParticleEmitter Emitter; public uint PartIndex; public Matrix4x4 Offset; } /// /// CPU-side mesh data prepared on a background thread. /// Contains vertex data and per-batch index/texture info, but NO GPU resources. /// public class ObjectMeshData { private long _estimatedUploadBytes = -1; public ulong ObjectId { get; set; } public bool IsSetup { get; set; } public VertexPositionNormalTexture[] Vertices { get; set; } = Array.Empty(); public List Batches { get; set; } = new(); /// /// #125 (2026-06-12): GL upload-retry counter. A failed upload through /// the App-side upload path (it returns null from its /// catch) used to be dropped permanently — the staged item was consumed, /// no render data was produced, and the prepared data lingered in the CPU /// cache where PrepareMeshDataAsync's cache-hit short-circuit /// returned it without ever re-staging it for upload (session-sticky /// invisible mesh, one [wb-error] line). The drain loop now re-stages a /// failed upload for the NEXT frame up to the App-side upload retry /// limit (MaxUploadRetries). The counter lives on the mesh-data /// object so /// it resets to 0 naturally whenever the id is re-prepared (fresh object), /// and bounds a deterministic GL failure to a few loud lines instead of a /// silent permanent drop OR an unbounded per-frame retry storm. Retail /// loads content synchronously and has no such failure mode — this /// converges our async pipeline toward that guarantee. /// public int UploadAttempts; /// For EnvCell: the geometry of the cell itself. public ObjectMeshData? EnvCellGeometry { get; set; } /// For Setup objects: parts with their local transforms. public List<(ulong GfxObjId, Matrix4x4 Transform)> SetupParts { get; set; } = new(); /// Particle emitters from physics scripts. public List ParticleEmitters { get; set; } = new(); /// Per-format texture atlas data (to be uploaded to GPU on main thread). public Dictionary<(int Width, int Height, TextureFormat Format), List> TextureBatches { get; set; } = new(); /// Local bounding box. public BoundingBox BoundingBox { get; set; } /// Approximate center point used for depth sorting / transparency ordering. public Vector3 SortCenter { get; set; } /// DataID of a simpler GfxObj to use at long distance / low quality, or GfxObjDegradeInfo. public uint DIDDegrade { get; set; } /// Sphere used for mouse selection. public Sphere? SelectionSphere { get; set; } /// Edge line vertices for Environment wireframe rendering. public Vector3[] EdgeLines { get; set; } = Array.Empty(); /// /// Returns the immutable prepared payload's CPU-to-GPU work estimate. /// Mesh extraction finishes populating the object before publishing it to /// App, so this value can be cached once and reused by the CPU cache, /// staging queue, retry path, and frame-budget planner without repeatedly /// walking every material batch under their locks. /// public long GetEstimatedUploadBytes() { long cached = System.Threading.Volatile.Read(ref _estimatedUploadBytes); if (cached >= 0) return cached; long bytes = IsSetup ? 1024L : 0L; bytes = checked(bytes + (long)Vertices.Length * VertexPositionNormalTexture.Size); foreach (List batches in TextureBatches.Values) { foreach (TextureBatchData batch in batches) { bytes = checked(bytes + batch.TextureData.LongLength); bytes = checked(bytes + (long)batch.Indices.Count * sizeof(ushort)); } } if (EnvCellGeometry is not null) bytes = checked(bytes + EnvCellGeometry.GetEstimatedUploadBytes()); System.Threading.Interlocked.CompareExchange(ref _estimatedUploadBytes, bytes, -1); return System.Threading.Volatile.Read(ref _estimatedUploadBytes); } } /// /// CPU-side data for a single rendering batch (indices + texture reference). /// public class MeshBatchData { public ushort[] Indices { get; set; } = Array.Empty(); public (int Width, int Height, TextureFormat Format) TextureFormat { get; set; } public TextureKey TextureKey { get; set; } public int TextureIndex { get; set; } public byte[] TextureData { get; set; } = Array.Empty(); public UploadPixelFormat? UploadPixelFormat { get; set; } public UploadPixelType? UploadPixelType { get; set; } public DatReaderWriter.Enums.CullMode CullMode { get; set; } } /// /// CPU-side texture info for deduplication during background preparation. /// public class TextureBatchData { public TextureKey Key { get; set; } public byte[] TextureData { get; set; } = Array.Empty(); public UploadPixelFormat? UploadPixelFormat { get; set; } public UploadPixelType? UploadPixelType { get; set; } public List Indices { get; set; } = new(); public DatReaderWriter.Enums.CullMode CullMode { get; set; } public bool IsTransparent { get; set; } public bool IsAdditive { get; set; } public bool HasWrappingUVs { get; set; } }