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