using Chorizite.Core.Lib; using Chorizite.Core.Render.Enums; using AcDream.Core.Meshing; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using System; using System.Collections.Generic; using System.Linq; 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. /// /// /// OH2/S1 (docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md): /// for a CellStruct/EnvCell shell batch ( true), /// is FIXED retail raster state applied AFTER /// geometry has already been fan-expanded per side/copy candidate — /// RenderMeshSubset @0x0059CA10 always draws a constructed cell /// shell subset D3DCULL_CW — it is NOT the authored /// Polygon.SidesType any more. The subset/material OWNER for a cell /// batch is instead , the retail source /// surface-array index (contract §3.6): two cell batches can share a /// resolved Surface DID/ and still be two distinct /// subsets, or vice versa. For an ordinary GfxObj batch, /// keeps its historical meaning and the four /// cell-only fields below stay at their neutral (non-cell) defaults — /// never /// sets them. /// 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 TranslucencyKind Translucency { get; set; } = TranslucencyKind.Opaque; public bool IsTransparent { get; set; } public bool IsAdditive { get; set; } public bool HasWrappingUVs { get; set; } /// /// Retail source surface-array index this subset was constructed from /// (contract §3.6) — the CellStruct subset/material OWNER, not the /// resolved Surface DID or texture identity. -1 for a non-cell (GfxObj) /// batch, where this concept does not apply. /// public int SourceSurfaceIndex { get; set; } = -1; /// /// Retail's isStippledOrAlphaedMask byte for this surface slot /// (contract §3.2): D3DPolyRender::ConstructMesh @0x0059DFA0's /// per-surface initial mask /// () plus /// every polygon's positive-surface stippling OR /// (). 0 /// (unused) for a non-cell (GfxObj) batch. /// public byte RetailSurfaceMask { get; set; } /// /// The resolved cell surface's raw Surface.Type bits (contract /// §2.2) — evidence for the built-EnvCell /// (Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 admission /// decision (RenderDeviceD3D::DrawEnvCell @0x0059F170 → /// D3DPolyRender::DrawMesh @0x0059D4A0, contract §4) that /// already happened before this batch was ever emitted into /// . 0 (unused) for a /// non-cell (GfxObj) batch. /// public uint RawSurfaceType { get; set; } /// /// True for a CellStruct/EnvCell shell batch built by /// . /// Ordinary GfxObj batches leave this false. /// public bool IsCellShell { get; set; } } /// /// OH2/S1 chunk-2 (contract §9 item 6): the one place that recovers a /// prepared CellStruct mesh's surface-array-index subset order. /// groups /// by (Width, Height, Format) for /// atlas/texture-dedup STORAGE only (contract §3.6 point 5) — the /// SEMANTIC subset order is always ascending /// , recoverable /// independent of that storage grouping or of dictionary/list iteration /// order. Downstream consumers (App draw dispatch, later OVERHAUL slices /// such as OH7's ordered draw stream) must read cell subset order through /// this helper rather than iterating TextureBatches directly. /// public static class CellSurfaceSubsets { /// /// Every in with /// set, ordered ascending by /// . An untextured /// slot that retail's built-EnvCell admission skipped /// () /// was never emitted into /// in the first place, so it is absent here too — this enumerates /// DRAWABLE subsets, not every constructed slot. /// This LINQ form allocates and is for bake, upload, and test time only /// (App uploads consume it once per mesh in /// ObjectMeshManager.OrderedUploadBatches). A per-frame consumer /// must read a precomputed order (the uploaded batch list already is one), /// never call this per frame. /// public static IEnumerable InAscendingSurfaceOrder(ObjectMeshData mesh) => mesh.TextureBatches.Values .SelectMany(batches => batches) .Where(batch => batch.IsCellShell) .OrderBy(batch => batch.SourceSurfaceIndex); }