feat(nativegen)!: refactor to struct-based native wrappers

Major overhaul of native wrapper generation for ufbx and nvtt.
Replaces all hand-written and class-based wrappers with auto-generated partial struct wrappers that directly expose native API methods via pointers. Introduces a new JSON-driven configuration system using "remaps" and "actions" for flexible parameter/return mapping and method routing. Removes legacy config sections and helper classes, focusing solely on method wrappers. Updates all usages and tests to use the new pointer-based API. Cleans up obsolete code and ensures resource management is handled via struct Dispose methods. The result is a thinner, more direct, and maintainable interop layer.

BREAKING CHANGE: All managed wrapper classes and helpers are removed in favor of struct-based pointer wrappers. API usage and resource management patterns have changed.
This commit is contained in:
2026-03-15 20:48:54 +09:00
parent 3e4084c42a
commit 6cadd8edeb
278 changed files with 5387 additions and 12057 deletions

View File

@@ -1,6 +1,8 @@
using Ghost.Nvtt;
using Ghost.Nvtt.Wrapper;
using Ghost.Test.Core;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Ghost.MicroTest;
@@ -14,7 +16,7 @@ namespace Ghost.MicroTest;
/// 4. sRGB conversion — converts to linear colour space.
/// 5. Mipmap count — verifies CountMipmaps() returns a sensible value.
/// 6. Compression — compresses to BC7 with in-memory output.
/// 7. Mip chain — generates and compresses the full mip chain.
/// 7. Mip chain — generates and compresses the full pMip chain.
/// 8. Error callback — installs a global message callback and verifies
/// it doesn't crash.
/// 9. Output to file — re-runs the full pipeline writing a real .dds file
@@ -23,6 +25,7 @@ namespace Ghost.MicroTest;
internal sealed unsafe class NvttBindingTest : ITest
{
private const string _IMAGE_PATH = @"C:/Users/Misaki/Downloads/Screenshot 2024-07-20 035047.png";
private static ReadOnlySpan<byte> ImagePathByte => @"C:/Users/Misaki/Downloads/Screenshot 2024-07-20 035047.png"u8;
private string _outputDdsPath = string.Empty;
@@ -38,26 +41,20 @@ internal sealed unsafe class NvttBindingTest : ITest
{
// ---- Test 1: Version ---------------------------------------------------
Console.Write("[Test 1] nvttVersion ... ");
var version = NvttGlobal.Version;
var version = Api.nvttVersion();
Assert(version > 0, $"Expected version > 0, got {version}");
Console.WriteLine($"OK (version = {version >> 16}.{(version >> 8) & 0xFF}.{version & 0xFF})");
// ---- Test 2: CUDA support query (must not crash) ----------------------
Console.Write("[Test 2] IsCudaSupported ... ");
var cuda = NvttGlobal.IsCudaSupported;
var cuda = Api.nvttIsCudaSupported();
Console.WriteLine($"OK (cuda = {cuda})");
// ---- Test 3: Global message callback ----------------------------------
Console.Write("[Test 3] SetMessageCallback ... ");
var callbackFired = 0;
using (var token = NvttGlobal.SetMessageCallback((severity, error, msg) =>
{
callbackFired++;
Console.WriteLine($"/n [NVTT] [{severity}] {error}: {msg}");
}))
{
// Just install + dispose — no assertion needed; must not throw.
}
var token = Api.nvttSetMessageCallback(&CallBack, &callbackFired);
Console.WriteLine($"OK (no crash, callback fired {callbackFired} times during install)");
// ---- Test 4: Surface creation + load ----------------------------------
@@ -65,81 +62,92 @@ internal sealed unsafe class NvttBindingTest : ITest
Assert(File.Exists(_IMAGE_PATH),
$"Image not found: '{_IMAGE_PATH}'. Edit _IMAGE_PATH before running.");
using var surface = new NvttSurfaceHandle();
var loaded = surface.Load(_IMAGE_PATH, out var hasAlpha);
var pSurface = NvttSurface.Create();
NvttBoolean hasAlpha;
var loaded = pSurface->Load(ImagePathByte, &hasAlpha, false, null);
Assert(loaded, "nvttSurfaceLoad returned false");
Assert(!surface.IsNull, "Surface is null after load");
Assert(surface.Width > 0 && surface.Height > 0,
$"Bad dimensions after load: {surface.Width}x{surface.Height}");
Console.WriteLine($"OK ({surface.Width}x{surface.Height}, hasAlpha={hasAlpha})");
Assert(pSurface != null, "Surface is null after load");
Assert(pSurface->Width() > 0 && pSurface->Height() > 0,
$"Bad dimensions after load: {pSurface->Width()}x{pSurface->Height()}");
Console.WriteLine($"OK ({pSurface->Width()}x{pSurface->Height()}, hasAlpha={hasAlpha})");
// ---- Test 5: Resize to power-of-two ≤ 512 ----------------------------
Console.Write("[Test 5] ResizeMakeSquare ... ");
surface.ResizeMakeSquare(512,
pSurface->ResizeMakeSquare(512,
NvttRoundMode.NVTT_RoundMode_ToPreviousPowerOfTwo,
NvttResizeFilter.NVTT_ResizeFilter_Box);
Assert(surface.Width <= 512 && surface.Height <= 512,
$"Expected ≤512 after resize, got {surface.Width}x{surface.Height}");
Assert(IsPowerOfTwo(surface.Width) && IsPowerOfTwo(surface.Height),
$"Expected power-of-two after resize, got {surface.Width}x{surface.Height}");
Console.WriteLine($"OK ({surface.Width}x{surface.Height})");
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
Assert(pSurface->Width() <= 512 && pSurface->Height() <= 512,
$"Expected ≤512 after resize, got {pSurface->Width()}x{pSurface->Height()}");
Assert(IsPowerOfTwo(pSurface->Width()) && IsPowerOfTwo(pSurface->Height()),
$"Expected power-of-two after resize, got {pSurface->Width()}x{pSurface->Height()}");
Console.WriteLine($"OK ({pSurface->Width()}x{pSurface->Height()})");
// ---- Test 6: sRGB → linear conversion ---------------------------------
Console.Write("[Test 6] ToLinearFromSrgb ... ");
surface.ToLinearFromSrgb(); // must not crash
pSurface->ToLinearFromSrgb(null); // must not crash
Console.WriteLine("OK");
// ---- Test 7: CountMipmaps ---------------------------------------------
Console.Write("[Test 7] CountMipmaps ... ");
var mipCount = surface.CountMipmaps();
var expectedMax = (int)Math.Log2(Math.Max(surface.Width, surface.Height)) + 1;
var mipCount = pSurface->CountMipmaps(1);
var expectedMax = (int)Math.Log2(Math.Max(pSurface->Width(), pSurface->Height())) + 1;
Assert(mipCount > 0 && mipCount <= expectedMax,
$"Unexpected mip count: {mipCount} (expected 1..{expectedMax})");
Console.WriteLine($"OK ({mipCount} levels)");
// ---- Test 8: In-memory BC7 compression + mip chain -------------------
// ---- Test 8: In-memory BC7 compression + pMip chain -------------------
Console.Write("[Test 8] Compress BC7 in-memory ... ");
long totalBytesReceived = 0;
var totalBytesReceived = 0L;
var imagesBegun = 0;
using var compOpts = new NvttCompressionOptionsHandle();
compOpts.Format = NvttFormat.NVTT_Format_BC7;
compOpts.Quality = NvttQuality.NVTT_Quality_Fastest;
var pCompOpts = NvttCompressionOptions.Create();
pCompOpts->SetFormat(NvttFormat.NVTT_Format_BC7);
pCompOpts->SetQuality(NvttQuality.NVTT_Quality_Fastest);
using var outOpts = new NvttOutputOptionsHandle();
outOpts.OutputHeader = true;
outOpts.Srgb = true;
outOpts.Container = NvttContainer.NVTT_Container_DDS10;
var pOutOpts = NvttOutputOptions.Create();
pOutOpts->SetOutputHeader(true);
pOutOpts->SetSrgbFlag(true);
pOutOpts->SetContainer(NvttContainer.NVTT_Container_DDS10);
outOpts.SetOutputHandler(
beginImage: (size, w, h, d, face, mip) =>
pOutOpts->SetOutputHandler(
(size, w, h, d, face, mip) =>
{
imagesBegun++;
},
outputData: (ptr, len) =>
(ptr, len) =>
{
totalBytesReceived += len;
return true;
},
endImage: null
null
);
outOpts.SetErrorHandler(err =>
pOutOpts->SetErrorHandler(err =>
Console.WriteLine($"/n [NVTT Error] {err}"));
using var ctx = new NvttContextHandle();
ctx.SetCudaAcceleration(false); // CPU only for the test
var pCtx = NvttContext.Create();
pCtx->SetCudaAcceleration(false); // CPU only for the test
using var mip = surface.Clone();
var headerOk = ctx.OutputHeader(mip, mipCount, compOpts, outOpts);
var pMip = pSurface->Clone();
var headerOk = pCtx->OutputHeader(pMip, mipCount, pCompOpts, pOutOpts);
Assert(headerOk, "OutputHeader returned false");
for (var level = 0; level < mipCount; level++)
{
var compressOk = ctx.Compress(mip, face: 0, mipmap: level, compOpts, outOpts);
var compressOk = pCtx->Compress(pMip, face: 0, mipmap: level, pCompOpts, pOutOpts);
Assert(compressOk, $"Compress returned false at mip level {level}");
if (level + 1 < mipCount)
mip.BuildNextMipmap(NvttMipmapFilter.NVTT_MipmapFilter_Kaiser);
{
pMip->BuildNextMipmapDefaults(NvttMipmapFilter.NVTT_MipmapFilter_Kaiser, 1, null);
}
}
Assert(imagesBegun == mipCount,
@@ -149,48 +157,75 @@ internal sealed unsafe class NvttBindingTest : ITest
Console.WriteLine($"OK ({imagesBegun} mips, {totalBytesReceived:N0} bytes total)");
// ---- Test 9: EstimateSize consistency ---------------------------------
Console.Write("[Test 9] EstimateSize ... ");
var estimated = ctx.EstimateSize(surface, mipCount, compOpts);
var estimated = pCtx->EstimateSize(pSurface, mipCount, pCompOpts);
// Estimate can differ from actual due to header overhead; just sanity-check it's > 0.
Assert(estimated > 0, $"EstimateSize returned {estimated}");
Console.WriteLine($"OK (estimated = {estimated:N0} bytes, actual = {totalBytesReceived:N0} bytes)");
// ---- Test 10: Output to real DDS file ---------------------------------
Console.Write("[Test 10] Compress to file ... ");
using var outOptsFile = new NvttOutputOptionsHandle();
outOptsFile.OutputHeader = true;
outOptsFile.Srgb = true;
outOptsFile.Container = NvttContainer.NVTT_Container_DDS10;
outOptsFile.FileName = _outputDdsPath;
var pOutOptsFile = NvttOutputOptions.Create();
pOutOptsFile->SetOutputHeader(true);
pOutOptsFile->SetSrgbFlag(true);
pOutOptsFile->SetContainer(NvttContainer.NVTT_Container_DDS10);
pOutOptsFile->SetFileName(Encoding.UTF8.GetBytes(_outputDdsPath));
using var ctxFile = new NvttContextHandle();
using var mipFile = surface.Clone();
var pCtxFile = NvttContext.Create();
var pMipFile = pSurface->Clone();
var fileHeaderOk = ctxFile.OutputHeader(mipFile, mipCount, compOpts, outOptsFile);
var fileHeaderOk = pCtxFile->OutputHeader(pMipFile, mipCount, pCompOpts, pOutOptsFile);
Assert(fileHeaderOk, "File OutputHeader returned false");
for (var level = 0; level < mipCount; level++)
{
var ok = ctxFile.Compress(mipFile, face: 0, mipmap: level, compOpts, outOptsFile);
var ok = pCtxFile->Compress(pMipFile, face: 0, mipmap: level, pCompOpts, pOutOptsFile);
Assert(ok, $"File Compress returned false at level {level}");
if (level + 1 < mipCount)
mipFile.BuildNextMipmap(NvttMipmapFilter.NVTT_MipmapFilter_Kaiser);
{
pMipFile->BuildNextMipmapDefaults(NvttMipmapFilter.NVTT_MipmapFilter_Kaiser, 1, null);
}
}
Assert(File.Exists(_outputDdsPath),
$"DDS output file was not created: {_outputDdsPath}");
Assert(File.Exists(_outputDdsPath), $"DDS output file was not created: {_outputDdsPath}");
var fileSize = new FileInfo(_outputDdsPath).Length;
Assert(fileSize > 128, $"DDS file suspiciously small: {fileSize} bytes");
Console.WriteLine($"OK ({fileSize:N0} bytes → {_outputDdsPath})");
Console.WriteLine("/n[NvttBindingTest] All tests PASSED.");
Console.WriteLine("[NvttBindingTest] All tests PASSED.");
pMipFile->Dispose();
pCtxFile->Dispose();
pOutOptsFile->Dispose();
pMip->Dispose();
pCtx->Dispose();
pOutOpts->Dispose();
pCompOpts->Dispose();
pSurface->Dispose();
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void CallBack(NvttSeverity severity, NvttError error, sbyte* msg, void* userData)
{
(*(int*)userData)++;
int i = 0;
while (msg[i] != 0)
{
i++;
}
Console.WriteLine($"/n [NVTT] [{severity}] {error}: {Encoding.UTF8.GetString((byte*)msg, i)}");
}
public void Cleanup()
{
// Leave the DDS file in place so you can inspect it.
Console.WriteLine($"/n[NvttBindingTest] Output DDS left at: {_outputDdsPath}");
Console.WriteLine($"[NvttBindingTest] Output DDS left at: {_outputDdsPath}");
}
// -------------------------------------------------------------------------
@@ -198,7 +233,9 @@ internal sealed unsafe class NvttBindingTest : ITest
private static void Assert(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException($"[ASSERTION FAILED] {message}");
}
}
private static bool IsPowerOfTwo(int n)

View File

@@ -3,69 +3,50 @@ using Ghost.Ufbx;
namespace Ghost.MicroTest;
internal class UfbxBindingTest : ITest
internal unsafe class UfbxBindingTest : ITest
{
private static ReadOnlySpan<byte> TestFilePath => "F:/c/Third Parties/ufbx/data/blender_340_z_up_7400_binary.fbx"u8;
public void Setup()
{
}
public void Run()
{
// Smoke-test LoadOpts heap-pointer shape (construct, set, read back, dispose)
using var opts = new LoadOpts();
opts.IgnoreAnimation = true;
opts.IgnoreEmbedded = true;
var load_Opts = new ufbx_load_opts();
var error = new ufbx_error();
// Load scene using the safe high-level wrapper (no unsafe, no fixed blocks)
using var scene = Scene.LoadFile(TestFilePath, opts);
var pScene = ufbx_scene.load_file_len("F:/c/Third Parties/ufbx/data/blender_340_z_up_7400_binary.fbx"u8, &load_Opts, &error);
// Enumerate nodes using the wrapper's NodeList (ref struct, no allocation)
for (var i = 0; i < scene.Nodes.Count; i++)
if (pScene == null)
{
var node = scene.Nodes[i];
if (node.IsRoot)
Console.WriteLine(error.description.ToString());
}
for (var i = 0u; i < pScene->nodes.count; i++)
{
var node = pScene->nodes.data[i];
if (node->is_root)
{
continue;
}
// node.Name is a string property — no manual ToString() needed
Console.WriteLine($"Object: {node.Name}");
Console.WriteLine($"Object: {node->name}");
if (node.HasMesh)
if (node->mesh != null)
{
Console.WriteLine($"-> mesh with {node.Mesh.NumFaces} faces");
Console.WriteLine($"-> mesh with {node->mesh->num_faces} faces");
Console.WriteLine($"-> mesh with positions: {node->local_transform.translation}");
}
for (var j = 0u; j < node->materials.count; j++)
{
var mat = node->materials.data[j];
Console.WriteLine("-> material: " + mat->name);
Console.WriteLine(" -> shader type: " + mat->shader_type);
Console.WriteLine(" -> texture count: " + mat->textures.count);
}
}
// Find a node by name using the new instance method (no unsafe, no fixed)
var rootNode = scene.FindNode("RootNode"u8);
if (!rootNode.IsNull)
{
Console.WriteLine($"Found root node: {rootNode.Name}");
}
// Find a material by name
var material = scene.FindMaterial("Material"u8);
if (!material.IsNull)
{
Console.WriteLine($"Found material: {material.Name}");
// Find a prop on the material's props using the instance method
var prop = material.Props.FindProp("DiffuseColor"u8);
if (!prop.IsNull)
{
Console.WriteLine($" DiffuseColor prop type: {prop.Type}");
}
}
// Find an anim stack
var animStack = scene.FindAnimStack("Take 001"u8);
if (!animStack.IsNull)
{
Console.WriteLine($"Found anim stack: {animStack.Name}");
}
pScene->free();
Console.WriteLine("Done.");
}