Added ufbx warper

This commit is contained in:
2026-03-15 02:19:40 +09:00
parent cce1cf7256
commit 3e4084c42a
232 changed files with 10989 additions and 55 deletions

View File

@@ -0,0 +1,91 @@
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Wrapper around an nvtt batch list — a list of (surface, face, mipmap,
/// outputOptions) tuples passed to <see cref="NvttContext.CompressBatch"/>.
/// </summary>
public sealed unsafe class NvttBatchListHandle : IDisposable
{
private NvttBatchList* _ptr;
/// <summary>Raw pointer - use only when calling the native API directly.</summary>
public NvttBatchList* Ptr => _ptr;
// -------------------------------------------------------------------------
// Construction / destruction
// -------------------------------------------------------------------------
public NvttBatchListHandle() => _ptr = Api.nvttCreateBatchList();
public void Dispose()
{
if (_ptr != null)
{
Api.nvttDestroyBatchList(_ptr);
_ptr = null;
}
}
// -------------------------------------------------------------------------
// Mutation
// -------------------------------------------------------------------------
/// <summary>Removes all items from the list.</summary>
public void Clear()
{
ThrowIfDisposed();
Api.nvttBatchListClear(_ptr);
}
/// <summary>
/// Appends an entry. The <paramref name="surface"/> and
/// <paramref name="outputOptions"/> must remain alive for the duration of
/// any subsequent <see cref="NvttContext.CompressBatch"/> call.
/// </summary>
public void Append(NvttSurfaceHandle surface, int face, int mipmap,
NvttOutputOptionsHandle outputOptions)
{
ThrowIfDisposed();
Api.nvttBatchListAppend(_ptr, surface.Ptr, face, mipmap,
outputOptions.Ptr);
}
// -------------------------------------------------------------------------
// Query
// -------------------------------------------------------------------------
/// <summary>Number of items currently in the list.</summary>
public uint Count
{
get { ThrowIfDisposed(); return Api.nvttBatchListGetSize(_ptr); }
}
/// <summary>
/// Returns the raw pointers for item <paramref name="index"/>.
/// The pointers are borrowed - do NOT dispose them.
/// </summary>
public void GetItem(uint index,
out NvttSurface* surface, out int face, out int mipmap,
out NvttOutputOptions* outputOptions)
{
ThrowIfDisposed();
NvttSurface* s;
NvttOutputOptions* o;
int f, m;
Api.nvttBatchListGetItem(_ptr, index, &s, &f, &m, &o);
surface = s;
face = f;
mipmap = m;
outputOptions = o;
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttBatchListHandle));
}
}
}

View File

@@ -0,0 +1,123 @@
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Controls how a surface is compressed - format, quality, pixel layout and
/// optional quantization settings.
/// </summary>
public sealed unsafe class NvttCompressionOptionsHandle : IDisposable
{
private NvttCompressionOptions* _ptr;
/// <summary>Raw pointer - use only when calling the native API directly.</summary>
public NvttCompressionOptions* Ptr => _ptr;
// -------------------------------------------------------------------------
// Construction / destruction
// -------------------------------------------------------------------------
public NvttCompressionOptionsHandle() => _ptr = Api.nvttCreateCompressionOptions();
public void Dispose()
{
if (_ptr != null)
{
Api.nvttDestroyCompressionOptions(_ptr);
_ptr = null;
}
}
// -------------------------------------------------------------------------
// Properties
// -------------------------------------------------------------------------
/// <summary>Target compressed format (e.g. BC1, BC7, ASTC …).</summary>
public NvttFormat Format
{
set { ThrowIfDisposed(); Api.nvttSetCompressionOptionsFormat(_ptr, value); }
}
/// <summary>Compression quality preset.</summary>
public NvttQuality Quality
{
set { ThrowIfDisposed(); Api.nvttSetCompressionOptionsQuality(_ptr, value); }
}
/// <summary>Pixel type for uncompressed RGB(A) output.</summary>
public NvttPixelType PixelType
{
set { ThrowIfDisposed(); Api.nvttSetCompressionOptionsPixelType(_ptr, value); }
}
/// <summary>Row-pitch alignment in bytes for uncompressed output.</summary>
public int PitchAlignment
{
set { ThrowIfDisposed(); Api.nvttSetCompressionOptionsPitchAlignment(_ptr, value); }
}
// -------------------------------------------------------------------------
// Methods
// -------------------------------------------------------------------------
/// <summary>Resets all options to their default values.</summary>
public void Reset()
{
ThrowIfDisposed();
Api.nvttResetCompressionOptions(_ptr);
}
/// <summary>
/// Sets per-channel importance weights used during block-compression error
/// minimisation.
/// </summary>
public void SetColorWeights(float red, float green, float blue, float alpha = 1f)
{
ThrowIfDisposed();
Api.nvttSetCompressionOptionsColorWeights(_ptr, red, green, blue, alpha);
}
/// <summary>
/// Configures a custom uncompressed pixel format by specifying the bit-depth
/// and per-channel bit masks.
/// </summary>
public void SetPixelFormat(uint bitCount, uint rMask, uint gMask, uint bMask, uint aMask)
{
ThrowIfDisposed();
Api.nvttSetCompressionOptionsPixelFormat(_ptr, bitCount, rMask, gMask, bMask, aMask);
}
/// <summary>
/// Enables or disables dithering and binary-alpha quantisation.
/// </summary>
/// <param name="colorDithering">Dither RGB channels.</param>
/// <param name="alphaDithering">Dither the alpha channel.</param>
/// <param name="binaryAlpha">Snap alpha to 0 or 255.</param>
/// <param name="alphaThreshold">Threshold used when <paramref name="binaryAlpha"/> is true.</param>
public void SetQuantization(bool colorDithering, bool alphaDithering, bool binaryAlpha,
int alphaThreshold = 127)
{
ThrowIfDisposed();
Api.nvttSetCompressionOptionsQuantization(
_ptr,
NvttInterop.ToNvtt(colorDithering),
NvttInterop.ToNvtt(alphaDithering),
NvttInterop.ToNvtt(binaryAlpha),
alphaThreshold);
}
/// <summary>Returns the D3D9 FourCC / D3DFORMAT value for the current settings.</summary>
public uint GetD3D9Format()
{
ThrowIfDisposed();
return Api.nvttGetCompressionOptionsD3D9Format(_ptr);
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttCompressionOptionsHandle));
}
}
}

View File

@@ -0,0 +1,229 @@
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Wrapper around the nvtt compression context — the central object that drives
/// the compression pipeline.
/// </summary>
public sealed unsafe class NvttContextHandle : IDisposable
{
private NvttContext* _ptr;
/// <summary>Raw pointer - use only when calling the native API directly.</summary>
public NvttContext* Ptr => _ptr;
// -------------------------------------------------------------------------
// Construction / destruction
// -------------------------------------------------------------------------
public NvttContextHandle() => _ptr = Api.nvttCreateContext();
public void Dispose()
{
if (_ptr != null)
{
Api.nvttDestroyContext(_ptr);
_ptr = null;
}
}
// -------------------------------------------------------------------------
// CUDA acceleration
// -------------------------------------------------------------------------
/// <summary>Enables or disables CUDA-accelerated compression.</summary>
public void SetCudaAcceleration(bool enable)
{
ThrowIfDisposed();
Api.nvttSetContextCudaAcceleration(_ptr, NvttInterop.ToNvtt(enable));
}
/// <summary>Returns <c>true</c> if CUDA acceleration is currently enabled.</summary>
public bool IsCudaAccelerationEnabled
{
get
{
ThrowIfDisposed();
return NvttInterop.ToBool(Api.nvttContextIsCudaAccelerationEnabled(_ptr));
}
}
// -------------------------------------------------------------------------
// Timing
// -------------------------------------------------------------------------
/// <summary>
/// Enables or disables internal timing collection.
/// <paramref name="detailLevel"/> controls the granularity (0 = off, higher = more detail).
/// </summary>
public void EnableTiming(bool enable, int detailLevel = 1)
{
ThrowIfDisposed();
Api.nvttContextEnableTiming(_ptr, NvttInterop.ToNvtt(enable), detailLevel);
}
/// <summary>
/// Returns the timing context owned by this nvtt context.
/// The pointer is borrowed - do NOT dispose it separately.
/// Returns <c>null</c> if timing was never enabled.
/// </summary>
public NvttTimingContext* GetTimingContextPtr()
{
ThrowIfDisposed();
return Api.nvttContextGetTimingContext(_ptr);
}
// -------------------------------------------------------------------------
// Estimate size
// -------------------------------------------------------------------------
/// <summary>
/// Estimates the compressed size in bytes for <paramref name="mipmapCount"/>
/// mip levels of <paramref name="img"/> using <paramref name="compressionOptions"/>.
/// </summary>
public int EstimateSize(NvttSurfaceHandle img, int mipmapCount,
NvttCompressionOptionsHandle compressionOptions)
{
ThrowIfDisposed();
return Api.nvttContextEstimateSize(_ptr, img.Ptr, mipmapCount,
compressionOptions.Ptr);
}
/// <summary>Estimates the compressed size for a cube map.</summary>
public int EstimateSizeCube(NvttCubeSurfaceHandle img, int mipmapCount,
NvttCompressionOptionsHandle compressionOptions)
{
ThrowIfDisposed();
return Api.nvttContextEstimateSizeCube(_ptr, img.Ptr, mipmapCount,
compressionOptions.Ptr);
}
/// <summary>Estimates the compressed size for raw-data dimensions.</summary>
public int EstimateSizeData(int w, int h, int d, int mipmapCount,
NvttCompressionOptionsHandle compressionOptions)
{
ThrowIfDisposed();
return Api.nvttContextEstimateSizeData(_ptr, w, h, d, mipmapCount,
compressionOptions.Ptr);
}
// -------------------------------------------------------------------------
// Output header
// -------------------------------------------------------------------------
/// <summary>
/// Writes the DDS / KTX header to <paramref name="outputOptions"/>.
/// Must be called once before compressing mip levels.
/// Returns <c>false</c> on failure.
/// </summary>
public bool OutputHeader(NvttSurfaceHandle img, int mipmapCount,
NvttCompressionOptionsHandle compressionOptions, NvttOutputOptionsHandle outputOptions)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttContextOutputHeader(_ptr, img.Ptr, mipmapCount,
compressionOptions.Ptr, outputOptions.Ptr));
}
/// <summary>Writes the header for a cube-map texture.</summary>
public bool OutputHeaderCube(NvttCubeSurfaceHandle img, int mipmapCount,
NvttCompressionOptionsHandle compressionOptions, NvttOutputOptionsHandle outputOptions)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttContextOutputHeaderCube(_ptr, img.Ptr, mipmapCount,
compressionOptions.Ptr, outputOptions.Ptr));
}
/// <summary>Writes the header using explicit dimensions instead of a surface.</summary>
public bool OutputHeaderData(NvttTextureType type, int w, int h, int d,
int mipmapCount, bool isNormalMap,
NvttCompressionOptionsHandle compressionOptions, NvttOutputOptionsHandle outputOptions)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttContextOutputHeaderData(_ptr, type, w, h, d, mipmapCount,
NvttInterop.ToNvtt(isNormalMap),
compressionOptions.Ptr, outputOptions.Ptr));
}
// -------------------------------------------------------------------------
// Compress
// -------------------------------------------------------------------------
/// <summary>
/// Compresses a single face/mip of <paramref name="img"/> and sends the
/// result to <paramref name="outputOptions"/>.
/// Returns <c>false</c> on failure.
/// </summary>
public bool Compress(NvttSurfaceHandle img, int face, int mipmap,
NvttCompressionOptionsHandle compressionOptions, NvttOutputOptionsHandle outputOptions)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttContextCompress(_ptr, img.Ptr, face, mipmap,
compressionOptions.Ptr, outputOptions.Ptr));
}
/// <summary>Compresses a single mip of a cube-map face.</summary>
public bool CompressCube(NvttCubeSurfaceHandle img, int mipmap,
NvttCompressionOptionsHandle compressionOptions, NvttOutputOptionsHandle outputOptions)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttContextCompressCube(_ptr, img.Ptr, mipmap,
compressionOptions.Ptr, outputOptions.Ptr));
}
/// <summary>Compresses a single mip from a raw float RGBA buffer.</summary>
public bool CompressData(int w, int h, int d, int face, int mipmap,
ReadOnlySpan<float> rgba,
NvttCompressionOptionsHandle compressionOptions, NvttOutputOptionsHandle outputOptions)
{
ThrowIfDisposed();
fixed (float* p = rgba)
{
return NvttInterop.ToBool(
Api.nvttContextCompressData(_ptr, w, h, d, face, mipmap, p,
compressionOptions.Ptr, outputOptions.Ptr));
}
}
/// <summary>
/// Compresses a batch of (surface, face, mipmap, outputOptions) entries
/// using the shared <paramref name="compressionOptions"/>.
/// Returns <c>false</c> on failure.
/// </summary>
public bool CompressBatch(NvttBatchListHandle batchList,
NvttCompressionOptionsHandle compressionOptions)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttContextCompressBatch(_ptr, batchList.Ptr,
compressionOptions.Ptr));
}
// -------------------------------------------------------------------------
// Quantize
// -------------------------------------------------------------------------
/// <summary>
/// Quantizes <paramref name="surface"/> in place according to
/// <paramref name="compressionOptions"/> (useful before compressing
/// to formats that only support limited bit depths).
/// </summary>
public void Quantize(NvttSurfaceHandle surface, NvttCompressionOptionsHandle compressionOptions)
{
ThrowIfDisposed();
Api.nvttContextQuantize(_ptr, surface.Ptr, compressionOptions.Ptr);
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttContextHandle));
}
}
}

View File

@@ -0,0 +1,227 @@
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Wrapper around an nvtt cube-map surface (six faces, optional mip chain).
///
/// Methods that return a new <see cref="NvttCubeSurface"/> transfer ownership
/// to the caller; dispose the result when done.
/// </summary>
public sealed unsafe class NvttCubeSurfaceHandle : IDisposable
{
private NvttCubeSurface* _ptr;
/// <summary>Raw pointer - use only when calling the native API directly.</summary>
public NvttCubeSurface* Ptr => _ptr;
// -------------------------------------------------------------------------
// Construction / destruction
// -------------------------------------------------------------------------
public NvttCubeSurfaceHandle() => _ptr = Api.nvttCreateCubeSurface();
/// <summary>Wraps an existing raw pointer (takes ownership; will destroy on dispose).</summary>
internal NvttCubeSurfaceHandle(NvttCubeSurface* existing) => _ptr = existing;
public void Dispose()
{
if (_ptr != null)
{
Api.nvttDestroyCubeSurface(_ptr);
_ptr = null;
}
}
// -------------------------------------------------------------------------
// Read-only properties
// -------------------------------------------------------------------------
/// <summary>Returns <c>true</c> when the cube surface holds no data.</summary>
public bool IsNull
{
get { ThrowIfDisposed(); return NvttInterop.ToBool(Api.nvttCubeSurfaceIsNull(_ptr)); }
}
/// <summary>Side length in pixels of each face.</summary>
public int EdgeLength
{
get { ThrowIfDisposed(); return Api.nvttCubeSurfaceEdgeLength(_ptr); }
}
/// <summary>Number of mip levels stored in this cube surface.</summary>
public int MipmapCount
{
get { ThrowIfDisposed(); return Api.nvttCubeSurfaceCountMipmaps(_ptr); }
}
// -------------------------------------------------------------------------
// Load / Save
// -------------------------------------------------------------------------
/// <summary>
/// Loads a cube map from disk.
/// <paramref name="mipmap"/> selects which mip level to load (-1 = all).
/// Returns <c>false</c> on failure.
/// </summary>
public bool Load(string fileName, int mipmap = 0)
{
ThrowIfDisposed();
Span<byte> buf = stackalloc byte[NvttInterop._MAX_STACK_PATH];
var utf8 = NvttInterop.ToUtf8(fileName, buf);
fixed (byte* p = utf8)
{
return NvttInterop.ToBool(Api.nvttCubeSurfaceLoad(_ptr, (sbyte*)p, mipmap));
}
}
/// <summary>Loads a cube map from a managed byte array. Returns <c>false</c> on failure.</summary>
public bool LoadFromMemory(ReadOnlySpan<byte> data, int mipmap = 0)
{
ThrowIfDisposed();
fixed (byte* p = data)
{
return NvttInterop.ToBool(
Api.nvttCubeSurfaceLoadFromMemory(_ptr, p, (ulong)data.Length, mipmap));
}
}
/// <summary>Saves the cube map to disk. Returns <c>false</c> on failure.</summary>
public bool Save(string fileName)
{
ThrowIfDisposed();
Span<byte> buf = stackalloc byte[NvttInterop._MAX_STACK_PATH];
var utf8 = NvttInterop.ToUtf8(fileName, buf);
fixed (byte* p = utf8)
{
return NvttInterop.ToBool(Api.nvttCubeSurfaceSave(_ptr, (sbyte*)p));
}
}
// -------------------------------------------------------------------------
// Face access
// -------------------------------------------------------------------------
/// <summary>
/// Returns the raw <see cref="NvttSurface"/> pointer for the given face
/// (05). The pointer is owned by this cube surface - do NOT dispose it.
/// </summary>
public NvttSurface* FacePtr(int face)
{
ThrowIfDisposed();
return Api.nvttCubeSurfaceFace(_ptr, face);
}
// -------------------------------------------------------------------------
// Fold / Unfold
// -------------------------------------------------------------------------
/// <summary>
/// Folds a cross-layout <see cref="NvttSurface"/> into this cube surface.
/// </summary>
public void Fold(NvttSurfaceHandle img, NvttCubeLayout layout)
{
ThrowIfDisposed();
Api.nvttCubeSurfaceFold(_ptr, img.Ptr, layout);
}
/// <summary>
/// Unfolds the cube surface into a flat cross-layout image.
/// Caller owns the returned surface.
/// </summary>
public NvttSurfaceHandle Unfold(NvttCubeLayout layout)
{
ThrowIfDisposed();
return new NvttSurfaceHandle(Api.nvttCubeSurfaceUnfold(_ptr, layout));
}
// -------------------------------------------------------------------------
// Query methods
// -------------------------------------------------------------------------
/// <summary>Returns the per-channel average for the given channel index.</summary>
public float Average(int channel)
{
ThrowIfDisposed();
return Api.nvttCubeSurfaceAverage(_ptr, channel);
}
/// <summary>Returns the min and max values of a channel across all faces.</summary>
public void Range(int channel, out float min, out float max)
{
ThrowIfDisposed();
float lo, hi;
Api.nvttCubeSurfaceRange(_ptr, channel, &lo, &hi);
min = lo;
max = hi;
}
// -------------------------------------------------------------------------
// Pixel operations
// -------------------------------------------------------------------------
/// <summary>Clamps a channel to [<paramref name="low"/>, <paramref name="high"/>].</summary>
public void Clamp(int channel, float low, float high)
{
ThrowIfDisposed();
Api.nvttCubeSurfaceClamp(_ptr, channel, low, high);
}
/// <summary>Applies gamma expansion (toLinear) to all faces.</summary>
public void ToLinear(float gamma)
{
ThrowIfDisposed();
Api.nvttCubeSurfaceToLinear(_ptr, gamma);
}
/// <summary>Applies gamma compression to all faces.</summary>
public void ToGamma(float gamma)
{
ThrowIfDisposed();
Api.nvttCubeSurfaceToGamma(_ptr, gamma);
}
// -------------------------------------------------------------------------
// Filtering (return new owned NvttCubeSurface)
// -------------------------------------------------------------------------
/// <summary>
/// Computes an irradiance-filtered cube map of the given <paramref name="size"/>.
/// Caller owns the result.
/// </summary>
public NvttCubeSurfaceHandle IrradianceFilter(int size, EdgeFixup fixup = EdgeFixup.NVTT_EdgeFixup_None)
{
ThrowIfDisposed();
return new NvttCubeSurfaceHandle(Api.nvttCubeSurfaceIrradianceFilter(_ptr, size, fixup));
}
/// <summary>
/// Computes a cosine-power (specular) filtered cube map.
/// Caller owns the result.
/// </summary>
public NvttCubeSurfaceHandle CosinePowerFilter(int size, float cosinePower,
EdgeFixup fixup = EdgeFixup.NVTT_EdgeFixup_None)
{
ThrowIfDisposed();
return new NvttCubeSurfaceHandle(
Api.nvttCubeSurfaceCosinePowerFilter(_ptr, size, cosinePower, fixup));
}
/// <summary>
/// Resamples the cube map to the given <paramref name="size"/> using fast bilinear resampling.
/// Caller owns the result.
/// </summary>
public NvttCubeSurfaceHandle FastResample(int size, EdgeFixup fixup = EdgeFixup.NVTT_EdgeFixup_None)
{
ThrowIfDisposed();
return new NvttCubeSurfaceHandle(Api.nvttCubeSurfaceFastResample(_ptr, size, fixup));
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttCubeSurfaceHandle));
}
}
}

View File

@@ -0,0 +1,183 @@
using System.Runtime.InteropServices;
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Static helpers wrapping global nvtt functions (version, CUDA detection,
/// error utilities, image comparison, mipmap helpers).
/// </summary>
public static unsafe class NvttGlobal
{
// -------------------------------------------------------------------------
// Library info
// -------------------------------------------------------------------------
/// <summary>Returns the nvtt library version as a packed uint (major*10000 + minor*100 + patch).</summary>
public static uint Version => Api.nvttVersion();
/// <summary>Returns <c>true</c> when a CUDA-capable GPU is available.</summary>
public static bool IsCudaSupported
=> NvttInterop.ToBool(Api.nvttIsCudaSupported());
// -------------------------------------------------------------------------
// Error strings
// -------------------------------------------------------------------------
/// <summary>Returns a human-readable string for <paramref name="error"/>.</summary>
public static string? ErrorString(NvttError error)
=> NvttInterop.FromUtf8(Api.nvttErrorString(error));
// -------------------------------------------------------------------------
// Global message callback
//
// The delegate type must be kept alive as long as the callback is registered.
// Store the returned token and dispose it to clear the callback.
// -------------------------------------------------------------------------
/// <summary>
/// Delegate type for the global nvtt message callback.
/// </summary>
public delegate void MessageCallback(
NvttSeverity severity, NvttError error, string? description);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeMessageCallback(
NvttSeverity severity, NvttError error, sbyte* description, void* userData);
/// <summary>
/// A registration token returned by <see cref="SetMessageCallback"/>.
/// Dispose to clear the callback and release the pinned delegate.
/// </summary>
public sealed class MessageCallbackToken : IDisposable
{
private NativeMessageCallback? _pinned;
internal MessageCallbackToken(NativeMessageCallback pinned)
=> _pinned = pinned;
public void Dispose()
{
if (_pinned != null)
{
// Clear the callback by registering null.
Api.nvttSetMessageCallback(null, null);
_pinned = null;
}
}
}
/// <summary>
/// Registers a managed message callback that nvtt calls for warnings and errors.
/// Returns a token; dispose the token to unregister.
/// </summary>
public static MessageCallbackToken SetMessageCallback(MessageCallback callback)
{
void native(NvttSeverity sev, NvttError err, sbyte* descPtr, void* _)
{
var desc = NvttInterop.FromUtf8(descPtr);
callback(sev, err, desc);
}
var fp = Marshal.GetFunctionPointerForDelegate(native);
Api.nvttSetMessageCallback(
(delegate* unmanaged[Cdecl]<NvttSeverity, NvttError, sbyte*, void*, void>)fp,
null);
return new MessageCallbackToken(native);
}
// -------------------------------------------------------------------------
// Image comparison (error metrics)
// -------------------------------------------------------------------------
/// <summary>RMS per-channel colour error between two surfaces.</summary>
public static float RmsError(NvttSurfaceHandle reference, NvttSurfaceHandle img,
NvttTimingContext* tc = null)
=> Api.nvttRmsError(reference.Ptr, img.Ptr, tc);
/// <summary>RMS alpha-channel error between two surfaces.</summary>
public static float RmsAlphaError(NvttSurfaceHandle reference, NvttSurfaceHandle img,
NvttTimingContext* tc = null)
=> Api.nvttRmsAlphaError(reference.Ptr, img.Ptr, tc);
/// <summary>RMS CIE-Lab perceptual error between two surfaces.</summary>
public static float RmsCIELabError(NvttSurfaceHandle reference, NvttSurfaceHandle img,
NvttTimingContext* tc = null)
=> Api.nvttRmsCIELabError(reference.Ptr, img.Ptr, tc);
/// <summary>Angular error between two normal-map surfaces.</summary>
public static float AngularError(NvttSurfaceHandle reference, NvttSurfaceHandle img,
NvttTimingContext* tc = null)
=> Api.nvttAngularError(reference.Ptr, img.Ptr, tc);
/// <summary>
/// Tone-mapped RMS error. Useful for HDR comparisons.
/// </summary>
public static float RmsToneMappedError(NvttSurfaceHandle reference, NvttSurfaceHandle img,
float exposure, NvttTimingContext* tc = null)
=> Api.nvttRmsToneMappedError(reference.Ptr, img.Ptr, exposure, tc);
// -------------------------------------------------------------------------
// Difference image
// -------------------------------------------------------------------------
/// <summary>
/// Returns a new surface containing the scaled per-pixel difference.
/// Caller owns the returned surface.
/// </summary>
public static NvttSurfaceHandle Diff(NvttSurfaceHandle reference, NvttSurfaceHandle img,
float scale, NvttTimingContext* tc = null)
=> new NvttSurfaceHandle(Api.nvttDiff(reference.Ptr, img.Ptr, scale, tc));
// -------------------------------------------------------------------------
// Histogram
// -------------------------------------------------------------------------
/// <summary>
/// Returns a new surface containing a histogram visualisation of
/// <paramref name="img"/> at the given dimensions.
/// Caller owns the returned surface.
/// </summary>
public static NvttSurfaceHandle Histogram(NvttSurfaceHandle img, int width, int height,
NvttTimingContext* tc = null)
=> new NvttSurfaceHandle(Api.nvttHistogram(img.Ptr, width, height, tc));
/// <summary>
/// Returns a new surface containing a histogram visualisation with an
/// explicit value range.
/// Caller owns the returned surface.
/// </summary>
public static NvttSurfaceHandle HistogramRange(NvttSurfaceHandle img,
float minRange, float maxRange, int width, int height,
NvttTimingContext* tc = null)
=> new NvttSurfaceHandle(
Api.nvttHistogramRange(img.Ptr, minRange, maxRange, width, height, tc));
// -------------------------------------------------------------------------
// Extent / mipmap helpers
// -------------------------------------------------------------------------
/// <summary>
/// Computes the target extent (power-of-two rounding, texture type clamping,
/// etc.) for a texture of the given dimensions.
/// Modifies <paramref name="width"/>, <paramref name="height"/> and
/// <paramref name="depth"/> in place.
/// </summary>
public static void GetTargetExtent(ref int width, ref int height, ref int depth,
int maxExtent, NvttRoundMode roundMode, NvttTextureType textureType,
NvttTimingContext* tc = null)
{
fixed (int* pw = &width, ph = &height, pd = &depth)
{
Api.nvttGetTargetExtent(pw, ph, pd, maxExtent, roundMode, textureType, tc);
}
}
/// <summary>
/// Returns the number of mip levels that can be generated for the given
/// base dimensions.
/// </summary>
public static int CountMipmaps(int w, int h, int d,
NvttTimingContext* tc = null)
=> Api.nvttCountMipmaps(w, h, d, tc);
}

View File

@@ -0,0 +1,67 @@
using System.Runtime.CompilerServices;
using System.Text;
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Internal helpers for converting between managed and unmanaged types.
/// </summary>
internal static unsafe class NvttInterop
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static NvttBoolean ToNvtt(bool value)
=> value ? NvttBoolean.NVTT_True : NvttBoolean.NVTT_False;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool ToBool(NvttBoolean value)
=> value != NvttBoolean.NVTT_False;
// -------------------------------------------------------------------------
// String to UTF-8 sbyte*
//
// Usage pattern:
// fixed (byte* ptr = NvttInterop.ToUtf8(str, stackalloc byte[MaxStack]))
// Api.nvttSomething((sbyte*)ptr);
//
// For paths longer than MaxStack bytes the helper falls back to a heap
// allocation via Encoding.UTF8.GetBytes. The Span<byte> overload lets the
// caller decide the stackalloc size.
// -------------------------------------------------------------------------
internal const int _MAX_STACK_PATH = 512;
/// <summary>
/// Encode <paramref name="value"/> as null-terminated UTF-8 into
/// <paramref name="buffer"/>. Returns the used portion (including the
/// null terminator). If the buffer is too small a new heap array is
/// returned instead.
/// </summary>
internal static Span<byte> ToUtf8(string value, Span<byte> buffer)
{
var needed = Encoding.UTF8.GetByteCount(value) + 1; // +1 for null term
if (needed > buffer.Length)
{
buffer = new byte[needed];
}
var written = Encoding.UTF8.GetBytes(value, buffer);
buffer[written] = 0; // null terminator
return buffer[..(written + 1)];
}
internal static string? FromUtf8(sbyte* ptr)
{
if (ptr == null)
{
return null;
}
var len = 0;
while (ptr[len] != 0)
{
len++;
}
return Encoding.UTF8.GetString((byte*)ptr, len);
}
}

View File

@@ -0,0 +1,210 @@
using System.Runtime.InteropServices;
namespace Ghost.Nvtt.Wrapper;
/// <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 NvttBoolean OutputDataDelegate(void* data, int size, NvttBoolean lastChunk);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void ErrorDelegate(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) =>
{
var ok = outputData?.Invoke((nint)data, size) ?? true;
return ok ? Ghost.Nvtt.NvttBoolean.NVTT_True : Ghost.Nvtt.NvttBoolean.NVTT_False;
};
var beginPtr = Marshal.GetFunctionPointerForDelegate(_beginImageDelegate);
var outputPtr = Marshal.GetFunctionPointerForDelegate(_outputDataDelegate);
Api.nvttSetOutputOptionsOutputHandler(
_ptr,
(delegate* unmanaged[Cdecl]<int, int, int, int, int, int, void>)beginPtr,
(delegate* unmanaged[Cdecl]<void*, int, NvttBoolean>)outputPtr,
IntPtr.Zero);
}
private void RebindErrorHandler()
{
var handler = _errorHandler;
_errorDelegate = error => handler?.Invoke(error);
var errorPtr = Marshal.GetFunctionPointerForDelegate(_errorDelegate);
Api.nvttSetOutputOptionsErrorHandler(
_ptr,
(delegate* unmanaged[Cdecl]<NvttError, void>)errorPtr);
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttOutputOptionsHandle));
}
}
}

View File

@@ -0,0 +1,712 @@
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Wrapper around a single 2-D / 3-D / cube-face image surface used as input
/// to the compression pipeline.
///
/// Most mutating methods accept an optional <paramref name="tc"/> timing
/// context. Pass <c>null</c> (the default) to skip timing.
/// </summary>
public sealed unsafe class NvttSurfaceHandle : IDisposable
{
private NvttSurface* _ptr;
/// <summary>Raw pointer - use only when calling the native API directly.</summary>
public NvttSurface* Ptr => _ptr;
// -------------------------------------------------------------------------
// Construction / destruction
// -------------------------------------------------------------------------
public NvttSurfaceHandle() => _ptr = Api.nvttCreateSurface();
/// <summary>Wraps an existing raw pointer (takes ownership; will destroy on dispose).</summary>
internal NvttSurfaceHandle(NvttSurface* existing) => _ptr = existing;
public void Dispose()
{
if (_ptr != null)
{
Api.nvttDestroySurface(_ptr);
_ptr = null;
}
}
// -------------------------------------------------------------------------
// Clone / sub-image
// -------------------------------------------------------------------------
/// <summary>Returns a deep copy of this surface.</summary>
public NvttSurfaceHandle Clone()
{
ThrowIfDisposed();
return new NvttSurfaceHandle(Api.nvttSurfaceClone(_ptr));
}
/// <summary>
/// Extracts a rectangular sub-region into a new <see cref="NvttSurfaceHandle"/>.
/// </summary>
public NvttSurfaceHandle CreateSubImage(
int x0, int x1, int y0, int y1, int z0, int z1,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
return new NvttSurfaceHandle(Api.nvttSurfaceCreateSubImage(_ptr, x0, x1, y0, y1, z0, z1, tc));
}
// -------------------------------------------------------------------------
// Read-only properties
// -------------------------------------------------------------------------
/// <summary>Returns <c>true</c> when the surface holds no data.</summary>
public bool IsNull
{
get { ThrowIfDisposed(); return NvttInterop.ToBool(Api.nvttSurfaceIsNull(_ptr)); }
}
/// <summary>Image width in pixels.</summary>
public int Width
{
get { ThrowIfDisposed(); return Api.nvttSurfaceWidth(_ptr); }
}
/// <summary>Image height in pixels.</summary>
public int Height
{
get { ThrowIfDisposed(); return Api.nvttSurfaceHeight(_ptr); }
}
/// <summary>Image depth (1 for 2-D textures).</summary>
public int Depth
{
get { ThrowIfDisposed(); return Api.nvttSurfaceDepth(_ptr); }
}
/// <summary>Texture dimensionality.</summary>
public NvttTextureType TextureType
{
get { ThrowIfDisposed(); return Api.nvttSurfaceType(_ptr); }
}
/// <summary>Whether the surface contains a normal map.</summary>
public bool IsNormalMap
{
get { ThrowIfDisposed(); return NvttInterop.ToBool(Api.nvttSurfaceIsNormalMap(_ptr)); }
}
// -------------------------------------------------------------------------
// Settable properties
// -------------------------------------------------------------------------
/// <summary>UV wrap mode used when filtering near edges.</summary>
public NvttWrapMode WrapMode
{
get { ThrowIfDisposed(); return Api.nvttSurfaceWrapMode(_ptr); }
}
/// <summary>Alpha mode interpretation.</summary>
public NvttAlphaMode AlphaMode
{
get { ThrowIfDisposed(); return Api.nvttSurfaceAlphaMode(_ptr); }
}
// -------------------------------------------------------------------------
// Query methods
// -------------------------------------------------------------------------
/// <summary>
/// Returns the number of mip levels that can be generated down to
/// <paramref name="minSize"/> pixels on the smallest side.
/// </summary>
public int CountMipmaps(int minSize = 1)
{
ThrowIfDisposed();
return Api.nvttSurfaceCountMipmaps(_ptr, minSize);
}
/// <summary>Alpha-test coverage for the given reference value and channel.</summary>
public float AlphaTestCoverage(float alphaRef, int alphaChannel = 3)
{
ThrowIfDisposed();
return Api.nvttSurfaceAlphaTestCoverage(_ptr, alphaRef, alphaChannel);
}
/// <summary>Per-channel average luminance.</summary>
public float Average(int channel, int alphaChannel = 3, float gamma = 2.2f)
{
ThrowIfDisposed();
return Api.nvttSurfaceAverage(_ptr, channel, alphaChannel, gamma);
}
/// <summary>
/// Returns a pointer to the raw float data for all four channels interleaved.
/// The span is valid only while this surface is alive.
/// </summary>
public Span<float> Data
{
get
{
ThrowIfDisposed();
var p = Api.nvttSurfaceData(_ptr);
var count = Width * Height * Depth * 4;
return p == null ? Span<float>.Empty : new Span<float>(p, count);
}
}
/// <summary>
/// Returns a pointer to the raw float data for a single channel (0=R,1=G,2=B,3=A).
/// The span is valid only while this surface is alive.
/// </summary>
public Span<float> Channel(int index)
{
ThrowIfDisposed();
var p = Api.nvttSurfaceChannel(_ptr, index);
var count = Width * Height * Depth;
return p == null ? Span<float>.Empty : new Span<float>(p, count);
}
/// <summary>Populates a histogram array for the given channel.</summary>
public void Histogram(int channel, float rangeMin, float rangeMax, Span<int> bins,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
fixed (int* b = bins)
{
Api.nvttSurfaceHistogram(_ptr, channel, rangeMin, rangeMax, bins.Length, b, tc);
}
}
/// <summary>Returns the minimum and maximum values of a channel.</summary>
public void Range(int channel, out float min, out float max,
int alphaChannel = 3, float alphaRef = 0f,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
float lo, hi;
Api.nvttSurfaceRange(_ptr, channel, &lo, &hi, alphaChannel, alphaRef, tc);
min = lo;
max = hi;
}
// -------------------------------------------------------------------------
// Load / Save
// -------------------------------------------------------------------------
/// <summary>Loads an image from disk. Returns <c>false</c> on failure.</summary>
public bool Load(string fileName, out bool hasAlpha, bool expectSigned = false,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Span<byte> buf = stackalloc byte[NvttInterop._MAX_STACK_PATH];
var utf8 = NvttInterop.ToUtf8(fileName, buf);
fixed (byte* p = utf8)
{
NvttBoolean nvAlpha;
var ok = NvttInterop.ToBool(
Api.nvttSurfaceLoad(_ptr, (sbyte*)p, &nvAlpha,
NvttInterop.ToNvtt(expectSigned), tc));
hasAlpha = NvttInterop.ToBool(nvAlpha);
return ok;
}
}
/// <summary>Loads an image from a managed byte array. Returns <c>false</c> on failure.</summary>
public bool LoadFromMemory(ReadOnlySpan<byte> data, out bool hasAlpha,
bool expectSigned = false, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
fixed (byte* p = data)
{
NvttBoolean nvAlpha;
var ok = NvttInterop.ToBool(
Api.nvttSurfaceLoadFromMemory(_ptr, p, (ulong)data.Length,
&nvAlpha, NvttInterop.ToNvtt(expectSigned), tc));
hasAlpha = NvttInterop.ToBool(nvAlpha);
return ok;
}
}
/// <summary>Saves the surface to disk. Returns <c>false</c> on failure.</summary>
public bool Save(string fileName, bool hasAlpha = false, bool hdr = false,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Span<byte> buf = stackalloc byte[NvttInterop._MAX_STACK_PATH];
var utf8 = NvttInterop.ToUtf8(fileName, buf);
fixed (byte* p = utf8)
{
return NvttInterop.ToBool(
Api.nvttSurfaceSave(_ptr, (sbyte*)p,
NvttInterop.ToNvtt(hasAlpha), NvttInterop.ToNvtt(hdr), tc));
}
}
// -------------------------------------------------------------------------
// Set image data
// -------------------------------------------------------------------------
/// <summary>Allocates an empty surface of the given dimensions.</summary>
public bool SetImage(int w, int h, int d = 1, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
return NvttInterop.ToBool(Api.nvttSurfaceSetImage(_ptr, w, h, d, tc));
}
/// <summary>Sets the surface from interleaved RGBA data.</summary>
public bool SetImageData(NvttInputFormat format, int w, int h, int d,
ReadOnlySpan<byte> data, bool unsignedToSigned = false,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
fixed (byte* p = data)
{
return NvttInterop.ToBool(
Api.nvttSurfaceSetImageData(_ptr, format, w, h, d, p,
NvttInterop.ToNvtt(unsignedToSigned), tc));
}
}
/// <summary>Sets the surface from separate RGBA channel planes.</summary>
public bool SetImageRGBA(NvttInputFormat format, int w, int h, int d,
ReadOnlySpan<byte> r, ReadOnlySpan<byte> g,
ReadOnlySpan<byte> b, ReadOnlySpan<byte> a,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
fixed (byte* pr = r, pg = g, pb = b, pa = a)
{
return NvttInterop.ToBool(
Api.nvttSurfaceSetImageRGBA(_ptr, format, w, h, d, pr, pg, pb, pa, tc));
}
}
/// <summary>Sets the surface from a compressed 2-D image.</summary>
public bool SetImage2D(NvttFormat format, int w, int h,
ReadOnlySpan<byte> data, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
fixed (byte* p = data)
{
return NvttInterop.ToBool(
Api.nvttSurfaceSetImage2D(_ptr, format, w, h, p, tc));
}
}
/// <summary>Sets the surface from a compressed 3-D image.</summary>
public bool SetImage3D(NvttFormat format, int w, int h, int d,
ReadOnlySpan<byte> data, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
fixed (byte* p = data)
{
return NvttInterop.ToBool(
Api.nvttSurfaceSetImage3D(_ptr, format, w, h, d, p, tc));
}
}
// -------------------------------------------------------------------------
// Resize / mipmap
// -------------------------------------------------------------------------
/// <summary>Resizes the surface to the exact dimensions given.</summary>
public void Resize(int w, int h, int d, NvttResizeFilter filter,
float filterWidth = 1f, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceResize(_ptr, w, h, d, filter, filterWidth, null, tc);
}
/// <summary>Resizes so that the longest extent is at most <paramref name="maxExtent"/>.</summary>
public void ResizeMax(int maxExtent, NvttRoundMode mode, NvttResizeFilter filter,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceResizeMax(_ptr, maxExtent, mode, filter, tc);
}
/// <summary>Resizes to a square texture with side at most <paramref name="maxExtent"/>.</summary>
public void ResizeMakeSquare(int maxExtent, NvttRoundMode mode, NvttResizeFilter filter,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceResizeMakeSquare(_ptr, maxExtent, mode, filter, tc);
}
/// <summary>Pads or crops the canvas to the given dimensions without resampling.</summary>
public void CanvasSize(int w, int h, int d, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceCanvasSize(_ptr, w, h, d, tc);
}
/// <summary>Returns <c>true</c> if a next mip level can still be generated.</summary>
public bool CanMakeNextMipmap(int minSize = 1)
{
ThrowIfDisposed();
return NvttInterop.ToBool(Api.nvttSurfaceCanMakeNextMipmap(_ptr, minSize));
}
/// <summary>Generates the next mip level in place (downsamples by 2).</summary>
public bool BuildNextMipmap(NvttMipmapFilter filter, int minSize = 1,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttSurfaceBuildNextMipmapDefaults(_ptr, filter, minSize, tc));
}
/// <summary>Generates the next mip level using a solid colour.</summary>
public bool BuildNextMipmapSolidColor(float r, float g, float b, float a,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
var color = stackalloc float[4] { r, g, b, a };
return NvttInterop.ToBool(
Api.nvttSurfaceBuildNextMipmapSolidColor(_ptr, color, tc));
}
// -------------------------------------------------------------------------
// Colour-space conversions
// -------------------------------------------------------------------------
/// <summary>Converts from sRGB to linear (per-channel).</summary>
public void ToLinearFromSrgb(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToLinearFromSrgb(_ptr, tc);
}
/// <summary>Converts from linear to sRGB (clamped).</summary>
public void ToSrgb(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToSrgb(_ptr, tc);
}
/// <summary>Converts from sRGB to linear (unclamped).</summary>
public void ToLinearFromSrgbUnclamped(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToLinearFromSrgbUnclamped(_ptr, tc);
}
/// <summary>Converts from linear to sRGB (unclamped).</summary>
public void ToSrgbUnclamped(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToSrgbUnclamped(_ptr, tc);
}
/// <summary>Applies gamma expansion (toLinear) to all channels.</summary>
public void ToLinear(float gamma, NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToLinear(_ptr, gamma, tc);
}
/// <summary>Applies gamma compression to all channels.</summary>
public void ToGamma(float gamma, NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToGamma(_ptr, gamma, tc);
}
/// <summary>Applies gamma expansion to a single channel.</summary>
public void ToLinearChannel(int channel, float gamma, NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToLinearChannel(_ptr, channel, gamma, tc);
}
/// <summary>Applies gamma compression to a single channel.</summary>
public void ToGammaChannel(int channel, float gamma, NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToGammaChannel(_ptr, channel, gamma, tc);
}
// -------------------------------------------------------------------------
// Pixel operations
// -------------------------------------------------------------------------
/// <summary>Applies a 4×4 colour transform matrix plus per-channel offset.</summary>
public void Transform(
float[] w0, float[] w1, float[] w2, float[] w3, float[] offset,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
fixed (float* pw0 = w0, pw1 = w1, pw2 = w2, pw3 = w3, po = offset)
{
Api.nvttSurfaceTransform(_ptr, pw0, pw1, pw2, pw3, po, tc);
}
}
/// <summary>Rearranges channels: result[0]=src[r], result[1]=src[g], etc.</summary>
public void Swizzle(int r, int g, int b, int a, NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceSwizzle(_ptr, r, g, b, a, tc);
}
/// <summary>Applies <c>x = x * scale + bias</c> to a single channel.</summary>
public void ScaleBias(int channel, float scale, float bias,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceScaleBias(_ptr, channel, scale, bias, tc);
}
/// <summary>Clamps a channel to [<paramref name="low"/>, <paramref name="high"/>].</summary>
public void Clamp(int channel, float low, float high, NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceClamp(_ptr, channel, low, high, tc);
}
/// <summary>Blends toward a constant RGBA colour by factor <paramref name="t"/>.</summary>
public void Blend(float r, float g, float b, float a, float t,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceBlend(_ptr, r, g, b, a, t, tc);
}
/// <summary>Multiplies RGB by alpha.</summary>
public void PremultiplyAlpha(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfacePremultiplyAlpha(_ptr, tc);
}
/// <summary>Divides RGB by alpha (with epsilon guard against divide-by-zero).</summary>
public void DemultiplyAlpha(float epsilon = 1e-6f, NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceDemultiplyAlpha(_ptr, epsilon, tc);
}
/// <summary>Converts to greyscale by weighted sum of channels.</summary>
public void ToGreyScale(float redScale, float greenScale, float blueScale,
float alphaScale, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceToGreyScale(_ptr, redScale, greenScale, blueScale, alphaScale, tc);
}
/// <summary>Fills the edge border of the surface with the given colour.</summary>
public void SetBorder(float r, float g, float b, float a,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceSetBorder(_ptr, r, g, b, a, tc);
}
/// <summary>Fills the entire surface with a constant colour.</summary>
public void Fill(float r, float g, float b, float a, NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceFill(_ptr, r, g, b, a, tc);
}
/// <summary>Scales alpha so that alpha-test coverage matches the given target.</summary>
public void ScaleAlphaToCoverage(float coverage, float alphaRef = 0.5f,
int alphaChannel = 3, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceScaleAlphaToCoverage(_ptr, coverage, alphaRef, alphaChannel, tc);
}
// -------------------------------------------------------------------------
// HDR encodings
// -------------------------------------------------------------------------
/// <summary>Encodes to RGBM (RGB * M, M in alpha).</summary>
public void ToRGBM(float range = 6f, float threshold = 0.25f,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToRGBM(_ptr, range, threshold, tc);
}
/// <summary>Decodes from RGBM back to linear HDR.</summary>
public void FromRGBM(float range = 6f, float threshold = 0.25f,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceFromRGBM(_ptr, range, threshold, tc);
}
/// <summary>Encodes to RGBE (Radiance HDR format).</summary>
public void ToRGBE(int mantissaBits = 9, int exponentBits = 5,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToRGBE(_ptr, mantissaBits, exponentBits, tc);
}
/// <summary>Decodes from RGBE back to linear HDR.</summary>
public void FromRGBE(int mantissaBits = 9, int exponentBits = 5,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceFromRGBE(_ptr, mantissaBits, exponentBits, tc);
}
/// <summary>Converts to YCoCg colour space.</summary>
public void ToYCoCg(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToYCoCg(_ptr, tc);
}
/// <summary>Converts from YCoCg back to RGB.</summary>
public void FromYCoCg(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceFromYCoCg(_ptr, tc);
}
// -------------------------------------------------------------------------
// Normal-map operations
// -------------------------------------------------------------------------
/// <summary>
/// Generates a normal map from the surface (treated as a height map)
/// using four blur kernel sizes.
/// </summary>
public void ToNormalMap(float sm, float medium, float big, float large,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceToNormalMap(_ptr, sm, medium, big, large, tc);
}
/// <summary>Re-normalises all normal vectors in the surface.</summary>
public void NormalizeNormalMap(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceNormalizeNormalMap(_ptr, tc);
}
/// <summary>Applies a normal-space transform.</summary>
public void TransformNormals(NvttNormalTransform xform,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceTransformNormals(_ptr, xform, tc);
}
/// <summary>Reconstructs normals from a packed representation.</summary>
public void ReconstructNormals(NvttNormalTransform xform,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceReconstructNormals(_ptr, xform, tc);
}
/// <summary>Packs normals into [0,1] range using <c>n*scale+bias</c>.</summary>
public void PackNormals(float scale = 0.5f, float bias = 0.5f,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfacePackNormals(_ptr, scale, bias, tc);
}
/// <summary>Expands packed normals back to [-1,1] range.</summary>
public void ExpandNormals(float scale = 2f, float bias = -1f,
NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceExpandNormals(_ptr, scale, bias, tc);
}
/// <summary>
/// Creates a Toksvig specular power map from a normal map.
/// Caller owns the returned surface.
/// </summary>
public NvttSurfaceHandle CreateToksvigMap(float power, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
return new NvttSurfaceHandle(Api.nvttSurfaceCreateToksvigMap(_ptr, power, tc));
}
// -------------------------------------------------------------------------
// Flip
// -------------------------------------------------------------------------
/// <summary>Flips the surface along the X axis.</summary>
public void FlipX(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceFlipX(_ptr, tc);
}
/// <summary>Flips the surface along the Y axis.</summary>
public void FlipY(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceFlipY(_ptr, tc);
}
/// <summary>Flips the surface along the Z axis.</summary>
public void FlipZ(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceFlipZ(_ptr, tc);
}
// -------------------------------------------------------------------------
// Channel copy / add
// -------------------------------------------------------------------------
/// <summary>Copies a single channel from another surface.</summary>
public bool CopyChannel(NvttSurfaceHandle src, int srcChannel, int dstChannel,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttSurfaceCopyChannel(_ptr, src.Ptr, srcChannel, dstChannel, tc));
}
/// <summary>Adds a scaled channel from another surface into this one.</summary>
public bool AddChannel(NvttSurfaceHandle src, int srcChannel, int dstChannel,
float scale = 1f, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttSurfaceAddChannel(_ptr, src.Ptr, srcChannel, dstChannel, scale, tc));
}
/// <summary>Copies a rectangular region from another surface into this one.</summary>
public bool Copy(NvttSurfaceHandle src,
int xsrc, int ysrc, int zsrc,
int xsize, int ysize, int zsize,
int xdst, int ydst, int zdst,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
return NvttInterop.ToBool(
Api.nvttSurfaceCopy(_ptr, src.Ptr,
xsrc, ysrc, zsrc, xsize, ysize, zsize, xdst, ydst, zdst, tc));
}
// -------------------------------------------------------------------------
// GPU transfer
// -------------------------------------------------------------------------
/// <summary>Uploads the surface to the GPU (CUDA). <paramref name="performCopy"/> clones instead of moving.</summary>
public void ToGPU(bool performCopy = false, NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceToGPU(_ptr, NvttInterop.ToNvtt(performCopy), tc);
}
/// <summary>Downloads the surface back to CPU memory.</summary>
public void ToCPU(NvttTimingContext* tc = null)
{
ThrowIfDisposed(); Api.nvttSurfaceToCPU(_ptr, tc);
}
// -------------------------------------------------------------------------
// Quantize / binarize
// -------------------------------------------------------------------------
/// <summary>Quantizes a channel to the given bit depth.</summary>
public void Quantize(int channel, int bits,
bool exactEndPoints = false, bool dither = false,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceQuantize(_ptr, channel, bits,
NvttInterop.ToNvtt(exactEndPoints), NvttInterop.ToNvtt(dither), tc);
}
/// <summary>Binarizes a channel using a threshold.</summary>
public void Binarize(int channel, float threshold, bool dither = false,
NvttTimingContext* tc = null)
{
ThrowIfDisposed();
Api.nvttSurfaceBinarize(_ptr, channel, threshold,
NvttInterop.ToNvtt(dither), tc);
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttSurfaceHandle));
}
}
}

View File

@@ -0,0 +1,144 @@
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Wrapper around an nvtt surface set — a collection of faces and mip levels
/// loaded from a DDS file or built programmatically.
/// </summary>
public sealed unsafe class NvttSurfaceSetHandle : IDisposable
{
private NvttSurfaceSet* _ptr;
/// <summary>Raw pointer - use only when calling the native API directly.</summary>
public NvttSurfaceSet* Ptr => _ptr;
// -------------------------------------------------------------------------
// Construction / destruction
// -------------------------------------------------------------------------
public NvttSurfaceSetHandle() => _ptr = Api.nvttCreateSurfaceSet();
public void Dispose()
{
if (_ptr != null)
{
Api.nvttDestroySurfaceSet(_ptr);
_ptr = null;
}
}
// -------------------------------------------------------------------------
// Read-only properties
// -------------------------------------------------------------------------
/// <summary>Texture dimensionality stored in this set.</summary>
public NvttTextureType TextureType
{
get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetTextureType(_ptr); }
}
/// <summary>Number of faces (1 for 2-D / 3-D, 6 for cube maps).</summary>
public int FaceCount
{
get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetFaceCount(_ptr); }
}
/// <summary>Number of mip levels.</summary>
public int MipmapCount
{
get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetMipmapCount(_ptr); }
}
/// <summary>Width of the base (mip 0) image in pixels.</summary>
public int Width
{
get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetWidth(_ptr); }
}
/// <summary>Height of the base (mip 0) image in pixels.</summary>
public int Height
{
get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetHeight(_ptr); }
}
/// <summary>Depth of the base (mip 0) image (1 for 2-D textures).</summary>
public int Depth
{
get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetDepth(_ptr); }
}
// -------------------------------------------------------------------------
// Surface access
// -------------------------------------------------------------------------
/// <summary>
/// Returns the raw <see cref="NvttSurface"/> pointer for the given face
/// and mip level. The pointer is owned by this surface set - do NOT dispose
/// it.
/// </summary>
public NvttSurface* GetSurfacePtr(int faceId, int mipId, bool expectSigned = false)
{
ThrowIfDisposed();
return Api.nvttSurfaceSetGetSurface(_ptr, faceId, mipId,
NvttInterop.ToNvtt(expectSigned));
}
// -------------------------------------------------------------------------
// Load / Save
// -------------------------------------------------------------------------
/// <summary>Resets the surface set to an empty state.</summary>
public void Reset()
{
ThrowIfDisposed();
Api.nvttResetSurfaceSet(_ptr);
}
/// <summary>Loads from a DDS file. Returns <c>false</c> on failure.</summary>
public bool LoadDDS(string fileName, bool forceNormal = false)
{
ThrowIfDisposed();
Span<byte> buf = stackalloc byte[NvttInterop._MAX_STACK_PATH];
var utf8 = NvttInterop.ToUtf8(fileName, buf);
fixed (byte* p = utf8)
{
return NvttInterop.ToBool(
Api.nvttSurfaceSetLoadDDS(_ptr, (sbyte*)p,
NvttInterop.ToNvtt(forceNormal)));
}
}
/// <summary>Loads from a managed byte array containing DDS data. Returns <c>false</c> on failure.</summary>
public bool LoadDDSFromMemory(ReadOnlySpan<byte> data, bool forceNormal = false)
{
ThrowIfDisposed();
fixed (byte* p = data)
{
return NvttInterop.ToBool(
Api.nvttSurfaceSetLoadDDSFromMemory(_ptr, p, (ulong)data.Length,
NvttInterop.ToNvtt(forceNormal)));
}
}
/// <summary>Saves a single face/mip as an image file. Returns <c>false</c> on failure.</summary>
public bool SaveImage(string fileName, int faceId, int mipId)
{
ThrowIfDisposed();
Span<byte> buf = stackalloc byte[NvttInterop._MAX_STACK_PATH];
var utf8 = NvttInterop.ToUtf8(fileName, buf);
fixed (byte* p = utf8)
{
return NvttInterop.ToBool(
Api.nvttSurfaceSetSaveImage(_ptr, (sbyte*)p, faceId, mipId));
}
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttSurfaceSetHandle));
}
}
}

View File

@@ -0,0 +1,99 @@
namespace Ghost.Nvtt.Wrapper;
/// <summary>
/// Wraps an nvtt timing context that records per-operation wall-clock times.
/// Obtain one from <see cref="NvttContext.TimingContext"/> or create a
/// standalone instance and pass it as the optional <c>tc</c> parameter on
/// surface methods.
/// </summary>
public sealed unsafe class NvttTimingContextHandle : IDisposable
{
private NvttTimingContext* _ptr;
/// <summary>Raw pointer - use only when calling the native API directly.</summary>
public NvttTimingContext* Ptr => _ptr;
// -------------------------------------------------------------------------
// Construction / destruction
// -------------------------------------------------------------------------
/// <summary>Creates a timing context at the specified detail level (0 = off, higher = more detail).</summary>
public NvttTimingContextHandle(int detailLevel = 1)
=> _ptr = Api.nvttCreateTimingContext(detailLevel);
/// <summary>Wraps an already-owned native pointer (ownership transferred to this object).</summary>
internal NvttTimingContextHandle(NvttTimingContext* owned) => _ptr = owned;
public void Dispose()
{
if (_ptr != null)
{
Api.nvttDestroyTimingContext(_ptr);
_ptr = null;
}
}
// -------------------------------------------------------------------------
// Properties
// -------------------------------------------------------------------------
/// <summary>
/// Sets the detail level (0 = disabled; higher values record more sub-operations).
/// </summary>
public int DetailLevel
{
set
{
ThrowIfDisposed();
Api.nvttTimingContextSetDetailLevel(_ptr, value);
}
}
/// <summary>Number of timing records captured so far.</summary>
public int RecordCount
{
get
{
ThrowIfDisposed();
return Api.nvttTimingContextGetRecordCount(_ptr);
}
}
// -------------------------------------------------------------------------
// Methods
// -------------------------------------------------------------------------
/// <summary>
/// Returns the description and elapsed seconds for the record at
/// <paramref name="index"/>.
/// </summary>
public (string description, double seconds) GetRecord(int index)
{
ThrowIfDisposed();
Span<byte> buf = stackalloc byte[256];
double seconds;
fixed (byte* p = buf)
{
Api.nvttTimingContextGetRecordSafe(_ptr, index, (sbyte*)p, (nuint)buf.Length, &seconds);
var desc = NvttInterop.FromUtf8((sbyte*)p) ?? string.Empty;
return (desc, seconds);
}
}
/// <summary>Prints all timing records to stdout via the native library.</summary>
public void PrintRecords()
{
ThrowIfDisposed();
Api.nvttTimingContextPrintRecords(_ptr);
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttTimingContextHandle));
}
}
}