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.
This commit is contained in:
2026-02-23 17:13:10 +09:00
parent 78e3b4ef31
commit 93c58fa7fb
91 changed files with 3124 additions and 313 deletions

View File

@@ -1,6 +1,211 @@
namespace Ghost.Nvtt
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
{
public partial struct NvttOutputOptions
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));
}
}
}