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; }
}