using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text.Json;
using Cthangover.Core.Utils;
using Godot;
using Cthangover.Live2D.Cubism.Core;
using Cthangover.Core.Mods;
namespace Cthangover.Live2D.Cubism.Framework
{
///
/// Wraps a Live2D CubismCore native model instance.
/// Handles .moc3 loading with the required 64-byte alignment, exposes
/// model data through direct pointers into CubismCore arrays, and manages
/// texture loading from external file providers.
///
/// Memory layout: .moc3 data and model instance memory are pinned via
/// GCHandle to satisfy CubismCore's alignment requirements (64-byte and
/// 16-byte respectively). The native model handle (_model) is an opaque
/// pointer passed to all CubismCore P/Invoke calls.
///
/// Implements to free pinned memory when the
/// model is no longer needed.
///
public class CubismNativeModel : IDisposable
{
private IntPtr _model;
private GCHandle _mocAlignedHandle;
private GCHandle _modelAlignedHandle;
// The Core stores the raw function pointer, so the delegate must live
// for the whole process — a collected delegate would crash native code.
private static readonly CubismCoreBindings.CsmLogDelegate NativeLogDelegate = OnNativeLog;
private static bool _nativeLogWired;
static CubismNativeModel()
{
WireNativeLog();
}
///
/// Routes native Core log output (the only channel for native-side errors:
/// invalid moc3, unsupported version, alignment failures) into GameLogger.
///
private static void WireNativeLog()
{
if (_nativeLogWired) return;
_nativeLogWired = true;
CubismCoreBindings.csmSetLogFunction(NativeLogDelegate);
}
private static void OnNativeLog(IntPtr messagePtr)
{
if (messagePtr == IntPtr.Zero) return;
string message = Marshal.PtrToStringAnsi(messagePtr) ?? string.Empty;
GameLogger.Log("LIVE2D-CORE", message);
}
private int _drawableCount;
private int _parameterCount;
///
/// Array of Godot instances loaded from the
/// model's texture references. Indexed by drawable texture index.
///
public ImageTexture[] Textures { get; private set; }
/// Number of drawables (renderable mesh parts) in the model.
public int DrawableCount => _drawableCount;
/// Number of parameters in the model.
public int ParameterCount => _parameterCount;
/// Canvas pixel width from the moc3. Available after .
public float CanvasWidth { get; private set; }
/// Canvas pixel height from the moc3. Available after .
public float CanvasHeight { get; private set; }
/// Pixels-per-unit scale for converting Cubism coordinates to pixel space.
public float PixelsPerUnit { get; private set; }
/// Whether the native model has been successfully initialized.
public bool IsLoaded => _model != IntPtr.Zero;
/// Raw native model handle passed to CubismCore functions.
public IntPtr ModelPtr => _model;
///
/// Loads raw .moc3 bytes into CubismCore.
/// Allocates a 64-byte aligned buffer, copies moc data, revives the moc,
/// allocates a 16-byte aligned model buffer, and initializes the model.
/// Throws on failure.
///
public unsafe void LoadMoc(byte[] mocBytes)
{
if (IsLoaded)
{
// Guard against a silent native-memory leak: handles are pinned and the
// previous moc/model are never freed, so a second LoadMoc on the same
// instance is always a bug in the caller.
throw new InvalidOperationException(
"LoadMoc: a moc is already loaded in this CubismNativeModel instance. " +
"Create a new instance to load another model.");
}
int alignedMocSize = (mocBytes.Length + CubismCoreBindings.AlignofMoc - 1)
/ CubismCoreBindings.AlignofMoc * CubismCoreBindings.AlignofMoc;
byte[] mocBuffer = new byte[alignedMocSize + CubismCoreBindings.AlignofMoc];
_mocAlignedHandle = GCHandle.Alloc(mocBuffer, GCHandleType.Pinned);
try
{
IntPtr mocPtr = _mocAlignedHandle.AddrOfPinnedObject();
long alignedAddr = (mocPtr.ToInt64() + CubismCoreBindings.AlignofMoc - 1)
/ CubismCoreBindings.AlignofMoc * CubismCoreBindings.AlignofMoc;
var alignedMocPtr = new IntPtr(alignedAddr);
Marshal.Copy(mocBytes, 0, alignedMocPtr, mocBytes.Length);
// csmReviveMocInPlace (verified by disassembly of the shipped DLL) hard-requires
// size % 64 == 0 and returns failure with '"size" is invalid.' otherwise. Cubism
// Editor always exports moc3 files whose length is a multiple of 64, but we do not
// rely on that: the buffer is already zero-initialized and 64-byte padded, so we
// pass the padded size — the Core reads only the real content, zero padding is safe.
uint mocBufferSize = (uint)alignedMocSize;
// Validate the moc3 buffer against the loaded Core before reviving it.
// Both checks use Core functions that were previously declared but never
// called — this gives diagnosable errors instead of a bare
// "csmReviveMocInPlace failed" for wrong-version or corrupted files.
uint mocVersion = CubismCoreBindings.csmGetMocVersion((void*)alignedMocPtr, mocBufferSize);
uint coreVersion = CubismCoreBindings.csmGetLatestMocVersion();
if (mocVersion == 0)
{
GameLogger.Log("LIVE2D", "LoadMoc: buffer is not a recognized .moc3 file (csmGetMocVersion returned 0)", LogLevel.Error);
throw new InvalidOperationException("The .moc3 buffer is not a recognized moc3 file");
}
if (mocVersion > coreVersion)
{
GameLogger.Log("LIVE2D",
$"LoadMoc: .moc3 version {mocVersion} is newer than Cubism Core {coreVersion} — ship a newer Live2DCubismCore.dll",
LogLevel.Error);
throw new InvalidOperationException(
$".moc3 version {mocVersion} is not supported by Cubism Core {coreVersion}");
}
if (CubismCoreBindings.csmHasMocConsistency((void*)alignedMocPtr, mocBufferSize) != 1)
{
GameLogger.Log("LIVE2D", "LoadMoc: .moc3 failed the consistency check (csmHasMocConsistency != 1) — data is corrupted", LogLevel.Error);
throw new InvalidOperationException(".moc3 data is inconsistent (csmHasMocConsistency failed)");
}
IntPtr moc = CubismCoreBindings.csmReviveMocInPlace(
(void*)alignedMocPtr, mocBufferSize);
if (moc == IntPtr.Zero)
{
GameLogger.Log("LIVE2D", "LoadMoc: csmReviveMocInPlace failed — .moc3 data is invalid or corrupted", LogLevel.Error);
throw new InvalidOperationException("csmReviveMocInPlace failed");
}
uint modelSize = CubismCoreBindings.csmGetSizeofModel(moc);
byte[] modelBuffer = new byte[modelSize + CubismCoreBindings.AlignofModel];
_modelAlignedHandle = GCHandle.Alloc(modelBuffer, GCHandleType.Pinned);
IntPtr modelPtr = _modelAlignedHandle.AddrOfPinnedObject();
long modelAlignedAddr = (modelPtr.ToInt64() + CubismCoreBindings.AlignofModel - 1)
/ CubismCoreBindings.AlignofModel * CubismCoreBindings.AlignofModel;
var alignedModelPtr = new IntPtr(modelAlignedAddr);
_model = CubismCoreBindings.csmInitializeModelInPlace(
moc, (void*)alignedModelPtr, modelSize);
if (_model == IntPtr.Zero)
{
GameLogger.Log("LIVE2D", "LoadMoc: csmInitializeModelInPlace failed — model memory allocation failed", LogLevel.Error);
throw new InvalidOperationException("csmInitializeModelInPlace failed");
}
}
catch
{
// Any failure after the Alloc calls must release the pinned buffers —
// otherwise the GCHandles (and the native memory they pin) leak for the
// rest of the process, because LoadMoc throws before Dispose is reachable.
if (_mocAlignedHandle.IsAllocated) _mocAlignedHandle.Free();
if (_modelAlignedHandle.IsAllocated) _modelAlignedHandle.Free();
_mocAlignedHandle = default;
_modelAlignedHandle = default;
throw;
}
var sizeInPixels = default(CubismCoreBindings.csmVector2);
var originInPixels = default(CubismCoreBindings.csmVector2);
float ppu = 0;
CubismCoreBindings.csmReadCanvasInfo(_model, &sizeInPixels, &originInPixels, &ppu);
CanvasWidth = sizeInPixels.X;
CanvasHeight = sizeInPixels.Y;
PixelsPerUnit = ppu;
_drawableCount = CubismCoreBindings.csmGetDrawableCount(_model);
_parameterCount = CubismCoreBindings.csmGetParameterCount(_model);
}
///
/// Loads a model from .model3.json bytes.
/// Parses the FileReferences section to find the .moc3 file,
/// calls , then loads all referenced textures
/// through the given file provider.
///
public void LoadFromProvider(byte[] model3JsonBytes, string modelDir, IModFileProvider provider)
{
var json = JsonDocument.Parse(model3JsonBytes);
var fileRefs = json.RootElement.GetProperty("FileReferences");
string mocRelPath = fileRefs.GetProperty("Moc").GetString();
var mocBytes = ReadFile(modelDir, mocRelPath, provider);
if (mocBytes == null)
{
GameLogger.Log("LIVE2D", $"LoadFromProvider: .moc3 not found '{mocRelPath}'", LogLevel.Error);
return;
}
LoadMoc(mocBytes);
var texturePaths = new List();
if (fileRefs.TryGetProperty("Textures", out var texArr))
{
foreach (var t in texArr.EnumerateArray())
texturePaths.Add(t.GetString());
}
LoadTextures(texturePaths, modelDir, provider);
}
private static byte[] ReadFile(string baseDir, string relativePath, IModFileProvider provider)
{
var normalized = relativePath.Replace('\\', '/');
var combined = string.IsNullOrEmpty(baseDir) ? normalized : $"{baseDir}/{normalized}";
var result = provider.ReadFileBinary(combined);
if (result == null)
GameLogger.Log("LIVE2D", $"ReadFile: provider returned null for '{combined}'", LogLevel.Warning);
return result;
}
private void LoadTextures(List paths, string modelDir, IModFileProvider provider)
{
Textures = new ImageTexture[paths.Count];
for (int i = 0; i < paths.Count; i++)
{
var texBytes = ReadFile(modelDir, paths[i], provider);
if (texBytes == null || texBytes.Length == 0)
{
GameLogger.Log("LIVE2D", $"LoadTextures: failed to read texture '{paths[i]}'", LogLevel.Warning);
continue;
}
var img = new Image();
Error err;
if (texBytes[0] == 0x89 && texBytes[1] == 0x50)
err = img.LoadPngFromBuffer(texBytes);
else
err = img.LoadJpgFromBuffer(texBytes);
if (err != Error.Ok)
{
GameLogger.Log("LIVE2D", $"LoadTextures: failed to decode '{paths[i]}' — error={err}", LogLevel.Warning);
continue;
}
if (img.GetFormat() != Image.Format.Rgba8)
img.Convert(Image.Format.Rgba8);
img.GenerateMipmaps();
var tex = ImageTexture.CreateFromImage(img);
Textures[i] = tex;
var w = img.GetWidth();
var h = img.GetHeight();
img.Dispose();
GameLogger.Log("LIVE2D", $"LoadTextures: loaded '{paths[i]}' ({texBytes.Length}b, {w}x{h})");
}
}
///
/// Calls csmUpdateModel on the native model.
/// Must be called after modifying parameters/parts/opacities and before
/// reading vertex positions for rendering.
///
public unsafe void UpdateModel()
{
if (_model == IntPtr.Zero) return;
CubismCoreBindings.csmUpdateModel(_model);
}
///
/// Resets the per-frame dynamic flags for all drawables.
/// Called at the start of each render update cycle.
///
public unsafe void ResetDynamicFlags()
{
if (_model == IntPtr.Zero) return;
CubismCoreBindings.csmResetDrawableDynamicFlags(_model);
}
/// Returns the drawable count from the native model.
public unsafe int GetDrawableCount() => _drawableCount;
/// Returns a pointer to the dynamic flags byte array (one byte per drawable).
public unsafe byte* GetDynamicFlags() =>
CubismCoreBindings.csmGetDrawableDynamicFlags(_model);
/// Returns a pointer to the constant flags byte array (blend mode, double-sided, etc.).
public unsafe byte* GetConstantFlags() =>
CubismCoreBindings.csmGetDrawableConstantFlags(_model);
/// Returns a pointer to the texture index array (one int per drawable).
public unsafe int* GetTextureIndices() =>
CubismCoreBindings.csmGetDrawableTextureIndices(_model);
/// Returns a pointer to the vertex count array (one int per drawable).
public unsafe int* GetVertexCounts() =>
CubismCoreBindings.csmGetDrawableVertexCounts(_model);
/// Returns a pointer array of csmVector2 arrays — vertex positions per drawable.
public unsafe CubismCoreBindings.csmVector2** GetVertexPositions() =>
CubismCoreBindings.csmGetDrawableVertexPositions(_model);
/// Returns a pointer array of csmVector2 arrays — UV coordinates per drawable.
public unsafe CubismCoreBindings.csmVector2** GetVertexUvs() =>
CubismCoreBindings.csmGetDrawableVertexUvs(_model);
/// Returns a pointer to the index count array (one int per drawable).
public unsafe int* GetIndexCounts() =>
CubismCoreBindings.csmGetDrawableIndexCounts(_model);
/// Returns a pointer array of ushort arrays — triangle indices per drawable.
public unsafe ushort** GetIndices() =>
CubismCoreBindings.csmGetDrawableIndices(_model);
/// Returns a pointer to the per-drawable opacity array (one float per drawable).
public unsafe float* GetOpacities() =>
CubismCoreBindings.csmGetDrawableOpacities(_model);
/// Returns a pointer to the draw order indices array.
public unsafe int* GetDrawOrders() =>
CubismCoreBindings.csmGetDrawableDrawOrders(_model);
/// Returns a pointer to the render order indices array.
public unsafe int* GetRenderOrders() =>
CubismCoreBindings.csmGetRenderOrders(_model);
/// Returns a pointer to the mask count array (one int per drawable).
public unsafe int* GetMaskCounts() =>
CubismCoreBindings.csmGetDrawableMaskCounts(_model);
/// Returns a pointer array of int arrays — mask drawable indices per drawable.
public unsafe int** GetMasks() =>
CubismCoreBindings.csmGetDrawableMasks(_model);
/// Returns a pointer to the per-drawable multiply color array.
public unsafe CubismCoreBindings.csmVector4* GetMultiplyColors() =>
CubismCoreBindings.csmGetDrawableMultiplyColors(_model);
/// Returns a pointer to the per-drawable screen color array.
public unsafe CubismCoreBindings.csmVector4* GetScreenColors() =>
CubismCoreBindings.csmGetDrawableScreenColors(_model);
/// Returns a pointer to the current parameter values array (one float per parameter).
public unsafe float* GetParameterValues() =>
CubismCoreBindings.csmGetParameterValues(_model);
/// Returns a pointer to the parameter minimum bounds array.
public unsafe float* GetParameterMinimumValues() =>
CubismCoreBindings.csmGetParameterMinimumValues(_model);
/// Returns a pointer to the parameter maximum bounds array.
public unsafe float* GetParameterMaximumValues() =>
CubismCoreBindings.csmGetParameterMaximumValues(_model);
/// Returns a pointer to the parameter default values array.
public unsafe float* GetParameterDefaultValues() =>
CubismCoreBindings.csmGetParameterDefaultValues(_model);
///
/// Returns a pointer array of null-terminated ANSI strings — parameter IDs.
/// Use to read individual strings.
///
public unsafe IntPtr GetParameterIds() =>
CubismCoreBindings.csmGetParameterIds(_model);
///
/// Reads a null-terminated ANSI string from a pointer array at the given index.
/// Each element is a byte pointer; reads until null terminator.
///
public static unsafe string ReadStringFromPtrArray(IntPtr ptrArray, int index)
{
var strPtr = *(byte**)IntPtr.Add(ptrArray, index * IntPtr.Size);
return Marshal.PtrToStringAnsi((IntPtr)strPtr);
}
/// Returns the number of parts from the native model.
public unsafe int GetPartCount() =>
CubismCoreBindings.csmGetPartCount(_model);
/// Returns a pointer to the per-part opacity array (one float per part).
public unsafe float* GetPartOpacities() =>
CubismCoreBindings.csmGetPartOpacities(_model);
/// Returns a pointer array of part ID strings.
public unsafe IntPtr GetPartIds() =>
CubismCoreBindings.csmGetPartIds(_model);
// Snapshot of the parameter values for the Load/Save bracket
// (port of CubismModel.cpp:878-910).
private float[] _savedParameterValues;
///
/// Порт CubismModel::SaveParameters: сохраняет текущие значения параметров в снимок.
/// Вызывается сразу после фазы моушена, чтобы аддитивные слои (выражения, дыхание,
/// look-инъекция) каждый кадр стартовали от одного и того же базового состояния
/// и не накапливались кадр-за-кадром.
///
public unsafe void SaveParameters()
{
if (_model == IntPtr.Zero) return;
var values = GetParameterValues();
if (_savedParameterValues == null || _savedParameterValues.Length != _parameterCount)
_savedParameterValues = new float[_parameterCount];
for (int i = 0; i < _parameterCount; i++)
_savedParameterValues[i] = values[i];
}
///
/// Порт CubismModel::LoadParameters: восстанавливает значения параметров из снимка.
/// До первого SaveParameters ничего не делает (как в SDK).
///
public unsafe void LoadParameters()
{
if (_model == IntPtr.Zero || _savedParameterValues == null) return;
var values = GetParameterValues();
var count = System.Math.Min(_parameterCount, _savedParameterValues.Length);
for (int i = 0; i < count; i++)
values[i] = _savedParameterValues[i];
}
///
/// Frees pinned memory handles. The native model handle is set to zero;
/// the underlying CubismCore memory was allocated inline and lives in
/// the pinned buffers — freeing those handles releases all model memory.
///
public void Dispose()
{
if (_mocAlignedHandle.IsAllocated) _mocAlignedHandle.Free();
if (_modelAlignedHandle.IsAllocated) _modelAlignedHandle.Free();
_model = IntPtr.Zero;
}
}
}