using System.Runtime.InteropServices; namespace Ghost.Nvtt; /// /// Configures where compressed data is written and how it is formatted. /// /// Managed callback delegates are stored in this object and passed to the /// native library via . /// The delegates are kept alive by pinned instances /// for the lifetime of this object. /// public sealed unsafe class NvttOutputOptionsHandle : IDisposable { private NvttOutputOptions* _ptr; /// Raw pointer – use only when calling the native API directly. public NvttOutputOptions* Ptr => _ptr; // ------------------------------------------------------------------------- // Managed callback storage // ------------------------------------------------------------------------- // Delegate types that match the native C signatures exactly. [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void BeginImageDelegate(int size, int width, int height, int depth, int face, int mipLevel); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate Ghost.Nvtt.Native.NvttBoolean OutputDataDelegate(void* data, int size, Ghost.Nvtt.Native.NvttBoolean lastChunk); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void ErrorDelegate(Ghost.Nvtt.Native.NvttError error); // Pinned delegate instances – must stay alive as long as native code may call them. private BeginImageDelegate? _beginImageDelegate; private OutputDataDelegate? _outputDataDelegate; private ErrorDelegate? _errorDelegate; // Managed user-facing callbacks. private Action? _beginImage; private Func? _outputData; private Action? _endImage; private Action? _errorHandler; // ------------------------------------------------------------------------- // Construction / destruction // ------------------------------------------------------------------------- public NvttOutputOptionsHandle() => _ptr = Api.nvttCreateOutputOptions(); public void Dispose() { if (_ptr != null) { Api.nvttDestroyOutputOptions(_ptr); _ptr = null; } // Release delegate references so GC can collect them. _beginImageDelegate = null; _outputDataDelegate = null; _errorDelegate = null; } // ------------------------------------------------------------------------- // Properties // ------------------------------------------------------------------------- /// /// Path of the output file. The file is created/overwritten when /// compression runs. /// public string FileName { set { ThrowIfDisposed(); Span buf = stackalloc byte[NvttInterop._MAX_STACK_PATH]; var utf8 = NvttInterop.ToUtf8(value, buf); fixed (byte* p = utf8) { Api.nvttSetOutputOptionsFileName(_ptr, (sbyte*)p); } } } /// /// Whether to write a DDS / KTX file header. Default is true. /// public bool OutputHeader { set { ThrowIfDisposed(); Api.nvttSetOutputOptionsOutputHeader(_ptr, NvttInterop.ToNvtt(value)); } } /// Container format (DDS or DDS10). public NvttContainer Container { set { ThrowIfDisposed(); Api.nvttSetOutputOptionsContainer(_ptr, value); } } /// Application-defined version number stored in the header. public int UserVersion { set { ThrowIfDisposed(); Api.nvttSetOutputOptionsUserVersion(_ptr, value); } } /// Sets the sRGB flag in the output header. public bool Srgb { set { ThrowIfDisposed(); Api.nvttSetOutputOptionsSrgbFlag(_ptr, NvttInterop.ToNvtt(value)); } } // ------------------------------------------------------------------------- // Methods // ------------------------------------------------------------------------- /// Resets all options to their default values. public void Reset() { ThrowIfDisposed(); Api.nvttResetOutputOptions(_ptr); } /// /// Installs managed callbacks that receive the compressed data stream. /// /// is called once per mip level before any /// data arrives: (size, width, height, depth, face, mipLevel). /// receives each chunk of compressed bytes. /// The first argument is an pointing to the data, the /// second is the byte count. Return true to continue, false /// to abort. /// is called once after the last chunk. /// /// Only one set of output callbacks can be active at a time; calling this /// method again replaces the previous ones. /// public void SetOutputHandler( Action? beginImage, Func? outputData, Action? endImage) { ThrowIfDisposed(); _beginImage = beginImage; _outputData = outputData; _endImage = endImage; RebindOutputHandler(); } /// /// Installs a managed error handler. The handler is invoked with the /// code whenever the native library encounters an /// error. /// public void SetErrorHandler(Action? handler) { ThrowIfDisposed(); _errorHandler = handler; RebindErrorHandler(); } // ------------------------------------------------------------------------- // Internal delegate trampolines (instance-bound via closures) // ------------------------------------------------------------------------- private void RebindOutputHandler() { // Capture current callbacks into local vars for the closure. var beginImage = _beginImage; var outputData = _outputData; _beginImageDelegate = (size, width, height, depth, face, mipLevel) => beginImage?.Invoke(size, width, height, depth, face, mipLevel); _outputDataDelegate = (data, size, lastChunk) => { bool ok = outputData?.Invoke((nint)data, size) ?? true; return ok ? Ghost.Nvtt.Native.NvttBoolean.NVTT_True : Ghost.Nvtt.Native.NvttBoolean.NVTT_False; }; nint beginPtr = Marshal.GetFunctionPointerForDelegate(_beginImageDelegate); nint outputPtr = Marshal.GetFunctionPointerForDelegate(_outputDataDelegate); Api.nvttSetOutputOptionsOutputHandler( _ptr, (delegate* unmanaged[Cdecl])beginPtr, (delegate* unmanaged[Cdecl])outputPtr, IntPtr.Zero); } private void RebindErrorHandler() { var handler = _errorHandler; _errorDelegate = error => handler?.Invoke(error); nint errorPtr = Marshal.GetFunctionPointerForDelegate(_errorDelegate); Api.nvttSetOutputOptionsErrorHandler( _ptr, (delegate* unmanaged[Cdecl])errorPtr); } // ------------------------------------------------------------------------- private void ThrowIfDisposed() { if (_ptr == null) { throw new ObjectDisposedException(nameof(NvttOutputOptionsHandle)); } } }