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

@@ -284,7 +284,6 @@ internal class TextureAssetHandler : IImportableAssetHandler
public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default)
{
// ---- 1. Probe image info -----------------------------------------------
var info = ImageInfo.FromStream(sourceStream);
if (info.BitsPerChannel <= 0)
{
@@ -296,93 +295,94 @@ internal class TextureAssetHandler : IImportableAssetHandler
var height = info.Height;
var colorComponents = info.ColorComponents;
// ---- 2. Decode pixels into a managed byte[] ----------------------------
byte[] pixelBytes;
if (isFloat)
{
using var image = ImageResultFloat.FromStream(sourceStream, colorComponents);
var span = MemoryMarshal.AsBytes(image.AsSpan());
pixelBytes = new byte[span.Length];
pixelBytes = ArrayPool<byte>.Shared.Rent(span.Length);
span.CopyTo(pixelBytes);
}
else
{
using var image = ImageResult.FromStream(sourceStream, colorComponents);
var span = MemoryMarshal.AsBytes(image.AsSpan());
pixelBytes = new byte[span.Length];
var span = image.AsSpan();
pixelBytes = ArrayPool<byte>.Shared.Rent(span.Length);
span.CopyTo(pixelBytes);
}
// ---- 3. Run NVTT compression on a thread-pool thread (side-effect only) -
// The cache path is derivable at any time from (id, settingsHash), so we
// do NOT store it in the asset file. LoadAsync/SaveAsync will recompute it.
var settings = new TextureAssetSettings();
await Task.Run(() =>
TextureProcessor.CompressToCache(
EditorApplication.CachesFolderPath,
id,
pixelBytes,
width,
height,
isFloat,
colorComponents,
settings),
token).ConfigureAwait(false);
// ---- 4. Write asset file: header + settings + raw image data -----------
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{
HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE,
};
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
var sizeResult = await WriteSettingsToStreamAsync(settings, targetStream, token).ConfigureAwait(false);
if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
}
// Content layout (all little-endian):
// int32 width
// int32 height
// byte isFloat (0 = byte, 1 = float)
// int32 colorComponents (cast of ColorComponents enum)
// byte[] pixelBytes
const int _CONTENT_HEADER_SIZE = 4 + 4 + 1 + 4; // 13 bytes
header.SettingsSize = sizeResult.Value;
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
header.ContentSize = _CONTENT_HEADER_SIZE + pixelBytes.Length;
// Write raw image content
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var contentHeader = ArrayPool<byte>.Shared.Rent(_CONTENT_HEADER_SIZE);
try
{
BitConverter.TryWriteBytes(contentHeader.AsSpan(0, 4), width);
BitConverter.TryWriteBytes(contentHeader.AsSpan(4, 4), height);
contentHeader[8] = isFloat ? (byte)1 : (byte)0;
BitConverter.TryWriteBytes(contentHeader.AsSpan(9, 4), (int)colorComponents);
var settings = new TextureAssetSettings();
await Task.Run(() =>
TextureProcessor.CompressToCache(
EditorApplication.CachesFolderPath,
id,
pixelBytes,
width,
height,
isFloat,
colorComponents,
settings),
token).ConfigureAwait(false);
await targetStream.WriteAsync(contentHeader.AsMemory(0, _CONTENT_HEADER_SIZE), token).ConfigureAwait(false);
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{
HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE,
};
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
var sizeResult = await WriteSettingsToStreamAsync(settings, targetStream, token).ConfigureAwait(false);
if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
}
// Content layout (all little-endian):
// int32 width
// int32 height
// byte isFloat (0 = byte, 1 = float)
// int32 colorComponents (cast of ColorComponents enum)
// byte[] pixelBytes
const int _CONTENT_HEADER_SIZE = 4 + 4 + 1 + 4; // 13 bytes
header.SettingsSize = sizeResult.Value;
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
header.ContentSize = _CONTENT_HEADER_SIZE + pixelBytes.Length;
// Write raw image content
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var contentHeader = ArrayPool<byte>.Shared.Rent(_CONTENT_HEADER_SIZE);
try
{
BitConverter.TryWriteBytes(contentHeader.AsSpan(0, 4), width);
BitConverter.TryWriteBytes(contentHeader.AsSpan(4, 4), height);
contentHeader[8] = isFloat ? (byte)1 : (byte)0;
BitConverter.TryWriteBytes(contentHeader.AsSpan(9, 4), (int)colorComponents);
await targetStream.WriteAsync(contentHeader.AsMemory(0, _CONTENT_HEADER_SIZE), token).ConfigureAwait(false);
}
finally
{
ArrayPool<byte>.Shared.Return(contentHeader);
}
await targetStream.WriteAsync(pixelBytes, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
// Patch header now that all sizes are known
targetStream.Seek(0, SeekOrigin.Begin);
AssetMetadata.WriteToStream(targetStream, ref header);
return Result.Success();
}
finally
{
ArrayPool<byte>.Shared.Return(contentHeader);
ArrayPool<byte>.Shared.Return(pixelBytes);
}
await targetStream.WriteAsync(pixelBytes, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
// Patch header now that all sizes are known
targetStream.Seek(0, SeekOrigin.Begin);
AssetMetadata.WriteToStream(targetStream, ref header);
return Result.Success();
}
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)

View File

@@ -1,9 +1,10 @@
using Ghost.Nvtt;
using Ghost.Nvtt.Wrapper;
using Misaki.HighPerformance.Image;
using Misaki.HighPerformance.LowLevel;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Ghost.Editor.Core.AssetHandler;
@@ -39,7 +40,6 @@ internal static unsafe class TextureProcessor
ColorComponents colorComponents,
TextureAssetSettings settings)
{
// --- derive cache path --------------------------------------------------
var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER);
Directory.CreateDirectory(cacheDir);
@@ -47,19 +47,16 @@ internal static unsafe class TextureProcessor
var cacheFileName = $"{assetId:N}_{settingsHash:X16}.dds";
var cachePath = Path.Combine(cacheDir, cacheFileName);
// --- check validity: same file name = same settings hash = already done -
if (File.Exists(cachePath))
{
return cachePath;
}
// --- delete any stale cache entries for this asset ----------------------
foreach (var stale in Directory.EnumerateFiles(cacheDir, $"{assetId:N}_*.dds"))
{
File.Delete(stale);
}
// --- run NVTT pipeline --------------------------------------------------
RunNvttPipeline(cachePath, pixelData, width, height, isFloat, colorComponents, settings);
return cachePath;
@@ -74,84 +71,75 @@ internal static unsafe class TextureProcessor
ColorComponents colorComponents,
TextureAssetSettings settings)
{
using var surface = new NvttSurfaceHandle();
using var compOpts = new NvttCompressionOptionsHandle();
using var outOpts = new NvttOutputOptionsHandle();
using var ctx = new NvttContextHandle();
using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create());
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
using var pOutOpts = new DisposablePtr<NvttOutputOptions>(NvttOutputOptions.Create());
using var pCtx = new DisposablePtr<NvttContext>(NvttContext.Create());
// ---- 1. load pixels into NVTT -----------------------------------------
// Misaki.HighPerformance.Image always decodes to RGBA channel order.
// Float images → RGBA_32F, byte images → BGRA_8UB.
// NOTE: NVTT BGRA_8UB expects Blue in byte[0]; stb decodes RGBA so we need
// to pass RGBA. There is no RGBA_8UB enum — we swizzle after load instead.
var inputFormat = isFloat
? NvttInputFormat.NVTT_InputFormat_RGBA_32F
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
surface.SetImageData(inputFormat, width, height, 1, pixelData);
fixed (void* pData = pixelData)
{
pSurface.Get()->SetImageData(inputFormat, width, height, 1, pData, NvttBoolean.NVTT_True, null);
}
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA,
// so channels R and B are swapped — fix with swizzle(2,1,0,3).
if (!isFloat)
{
surface.Swizzle(2, 1, 0, 3);
pSurface.Get()->Swizzle(2, 1, 0, 3, null);
}
// ---- 2. resize ---------------------------------------------------------
var maxExtent = (int)settings.Sampler.MaxSize;
if (settings.Advanced.StretchToPowerOfTwo)
{
surface.ResizeMakeSquare(maxExtent,
pSurface.Get()->ResizeMakeSquare(maxExtent,
NvttRoundMode.NVTT_RoundMode_ToNearestPowerOfTwo,
NvttResizeFilter.NVTT_ResizeFilter_Box);
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
}
else if (surface.Width > maxExtent || surface.Height > maxExtent)
else if (pSurface.Get()->Width() > maxExtent || pSurface.Get()->Height() > maxExtent)
{
surface.ResizeMax(maxExtent,
pSurface.Get()->ResizeMax(maxExtent,
NvttRoundMode.NVTT_RoundMode_None,
NvttResizeFilter.NVTT_ResizeFilter_Box);
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
}
// ---- 2b. border color --------------------------------------------------
if (settings.Advanced.UseBorderColor)
{
var c = settings.Advanced.BorderColor;
surface.SetBorder(c.r, c.g, c.b, c.a);
pSurface.Get()->SetBorder(c.r, c.g, c.b, c.a, null);
}
else if (settings.Advanced.ZeroAlphaBorder)
{
surface.SetBorder(0f, 0f, 0f, 0f);
pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null);
}
// ---- 3. colour-space: convert to linear before mip filtering -----------
if (settings.Basic.IsSRGB && settings.Advanced.GammaCorrection)
{
surface.ToLinearFromSrgb();
pSurface.Get()->ToLinearFromSrgb(null);
}
// ---- 4. premultiply alpha (before mip chain) ---------------------------
if (settings.Advanced.PremultiplyAlpha)
{
surface.PremultiplyAlpha();
pSurface.Get()->PremultiplyAlpha(null);
}
// ---- 5. configure compression options ----------------------------------
compOpts.Format = SelectFormat(settings);
compOpts.Quality = SelectQuality(settings.Advanced.CompressionLevel);
pCompOpts.Get()->SetFormat(SelectFormat(settings));
pCompOpts.Get()->SetQuality(SelectQuality(settings.Advanced.CompressionLevel));
if (settings.Advanced.CutoutAlpha)
{
compOpts.SetQuantization(false, false, true,
pCompOpts.Get()->SetQuantization(false, false, true,
settings.Advanced.CutoutAlphaThreshold);
}
// ---- 6. configure output options ---------------------------------------
outOpts.OutputHeader = true;
outOpts.Srgb = settings.Basic.IsSRGB;
outOpts.Container = NvttContainer.NVTT_Container_DDS10;
outOpts.FileName = outputPath;
pOutOpts.Get()->SetOutputHeader(true);
pOutOpts.Get()->SetSrgbFlag(settings.Basic.IsSRGB);
pOutOpts.Get()->SetContainer(NvttContainer.NVTT_Container_DDS10);
pOutOpts.Get()->SetFileName(Encoding.UTF8.GetBytes(outputPath));
// ---- 7. mipmap count ---------------------------------------------------
var nvttFilter = SelectMipmapFilter(settings.Advanced.MipmapFilter);
int mipmapCount;
@@ -161,38 +149,35 @@ internal static unsafe class TextureProcessor
}
else if (settings.Advanced.MipmapLevelCount == 0)
{
mipmapCount = surface.CountMipmaps();
mipmapCount = pSurface.Get()->CountMipmaps(1);
}
else
{
mipmapCount = (int)settings.Advanced.MipmapLevelCount;
}
// ---- 8. enable CUDA if available ---------------------------------------
ctx.SetCudaAcceleration(NvttGlobal.IsCudaSupported);
pCtx.Get()->SetCudaAcceleration(Api.nvttIsCudaSupported());
// ---- 9. write DDS header -----------------------------------------------
ctx.OutputHeader(surface, mipmapCount, compOpts, outOpts);
pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get());
// ---- 10. compress mip chain using a working clone ----------------------
using var mip = surface.Clone();
using var pMip = new DisposablePtr<NvttSurface>(pSurface.Get()->Clone());
for (var level = 0; level < mipmapCount; level++)
{
// Scale alpha for coverage on each mip (if requested)
// Scale alpha for coverage on each pMip (if requested)
if (settings.Advanced.ScaleAlphaForMipCoverage && level > 0)
{
var refCoverage = mip.AlphaTestCoverage(
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f);
mip.ScaleAlphaToCoverage(refCoverage,
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f);
var refCoverage = pMip.Get()->AlphaTestCoverage(
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3);
pMip.Get()->ScaleAlphaToCoverage(refCoverage,
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null);
}
ctx.Compress(mip, 0, level, compOpts, outOpts);
pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get());
if (level + 1 < mipmapCount)
{
mip.BuildNextMipmap(nvttFilter);
pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null);
}
}
}
@@ -203,7 +188,7 @@ internal static unsafe class TextureProcessor
TextureType.Normal => NvttFormat.NVTT_Format_BC5, // RG normal map
TextureType.SingleChannel => NvttFormat.NVTT_Format_BC4, // single channel
TextureType.Lightmap => NvttFormat.NVTT_Format_BC6U, // HDR lightmap (unsigned)
_ => NvttFormat.NVTT_Format_BC7, // default colour
_ => NvttFormat.NVTT_Format_BC7, // default color
};
private static NvttQuality SelectQuality(TextureCompressionLevel level)