Files
GhostEngine/src/ThridParty/Ghost.Nvtt/NvttOutputOptions.cs
Misaki 93c58fa7fb Add Ghost.Nvtt C# wrapper and integrate nvtt texture pipeline
- Introduce full managed C# wrapper for NVIDIA Texture Tools (nvtt) with safe handle classes, idiomatic APIs, and managed callback support.
- Integrate Ghost.Nvtt into Ghost.Editor.Core and Ghost.MicroTest; update TextureAssetHandler to use the new nvtt wrapper for texture compression.
- Add comprehensive end-to-end binding test (NvttBindingTest).
- Refactor D3D12 resource management: add deferred/immediate release APIs, update allocator/database usage, and ensure proper resource cleanup.
- Update project files for new native DLL layout and dependency versions.
- Minor API cleanups: EditorApplication properties, D3D12 input layout, and removal of obsolete code.
- Update shaders, tests, and documentation for new APIs and usage patterns.
2026-02-23 17:13:10 +09:00

212 lines
7.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
namespace Ghost.Nvtt;
/// <summary>
/// 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 <see cref="Marshal.GetFunctionPointerForDelegate"/>.
/// The delegates are kept alive by pinned <see cref="GCHandle"/> instances
/// for the lifetime of this object.
/// </summary>
public sealed unsafe class NvttOutputOptionsHandle : IDisposable
{
private NvttOutputOptions* _ptr;
/// <summary>Raw pointer use only when calling the native API directly.</summary>
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<int, int, int, int, int, int>? _beginImage;
private Func<nint, int, bool>? _outputData;
private Action? _endImage;
private Action<NvttError>? _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
// -------------------------------------------------------------------------
/// <summary>
/// Path of the output file. The file is created/overwritten when
/// compression runs.
/// </summary>
public string FileName
{
set
{
ThrowIfDisposed();
Span<byte> buf = stackalloc byte[NvttInterop._MAX_STACK_PATH];
var utf8 = NvttInterop.ToUtf8(value, buf);
fixed (byte* p = utf8)
{
Api.nvttSetOutputOptionsFileName(_ptr, (sbyte*)p);
}
}
}
/// <summary>
/// Whether to write a DDS / KTX file header. Default is <c>true</c>.
/// </summary>
public bool OutputHeader
{
set { ThrowIfDisposed(); Api.nvttSetOutputOptionsOutputHeader(_ptr, NvttInterop.ToNvtt(value)); }
}
/// <summary>Container format (DDS or DDS10).</summary>
public NvttContainer Container
{
set { ThrowIfDisposed(); Api.nvttSetOutputOptionsContainer(_ptr, value); }
}
/// <summary>Application-defined version number stored in the header.</summary>
public int UserVersion
{
set { ThrowIfDisposed(); Api.nvttSetOutputOptionsUserVersion(_ptr, value); }
}
/// <summary>Sets the sRGB flag in the output header.</summary>
public bool Srgb
{
set { ThrowIfDisposed(); Api.nvttSetOutputOptionsSrgbFlag(_ptr, NvttInterop.ToNvtt(value)); }
}
// -------------------------------------------------------------------------
// Methods
// -------------------------------------------------------------------------
/// <summary>Resets all options to their default values.</summary>
public void Reset()
{
ThrowIfDisposed();
Api.nvttResetOutputOptions(_ptr);
}
/// <summary>
/// Installs managed callbacks that receive the compressed data stream.
///
/// <para><paramref name="beginImage"/> is called once per mip level before any
/// data arrives: <c>(size, width, height, depth, face, mipLevel)</c>.</para>
/// <para><paramref name="outputData"/> receives each chunk of compressed bytes.
/// The first argument is an <see cref="nint"/> pointing to the data, the
/// second is the byte count. Return <c>true</c> to continue, <c>false</c>
/// to abort.</para>
/// <para><paramref name="endImage"/> is called once after the last chunk.</para>
///
/// Only one set of output callbacks can be active at a time; calling this
/// method again replaces the previous ones.
/// </summary>
public void SetOutputHandler(
Action<int, int, int, int, int, int>? beginImage,
Func<nint, int, bool>? outputData,
Action? endImage)
{
ThrowIfDisposed();
_beginImage = beginImage;
_outputData = outputData;
_endImage = endImage;
RebindOutputHandler();
}
/// <summary>
/// Installs a managed error handler. The handler is invoked with the
/// <see cref="NvttError"/> code whenever the native library encounters an
/// error.
/// </summary>
public void SetErrorHandler(Action<NvttError>? 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]<int, int, int, int, int, int, void>)beginPtr,
(delegate* unmanaged[Cdecl]<void*, int, Ghost.Nvtt.Native.NvttBoolean>)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]<Ghost.Nvtt.Native.NvttError, void>)errorPtr);
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttOutputOptionsHandle));
}
}
}