forked from Misaki/GhostEngine
Add Ghost.Nvtt C# wrapper and integrate nvtt texture pipeline
- Introduce full managed C# wrapper for NVIDIA Texture Tools (nvtt) with safe handle classes, idiomatic APIs, and managed callback support. - Integrate Ghost.Nvtt into Ghost.Editor.Core and Ghost.MicroTest; update TextureAssetHandler to use the new nvtt wrapper for texture compression. - Add comprehensive end-to-end binding test (NvttBindingTest). - Refactor D3D12 resource management: add deferred/immediate release APIs, update allocator/database usage, and ensure proper resource cleanup. - Update project files for new native DLL layout and dependency versions. - Minor API cleanups: EditorApplication properties, D3D12 input layout, and removal of obsolete code. - Update shaders, tests, and documentation for new APIs and usage patterns.
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
@@ -178,8 +176,4 @@ public readonly struct AssetReference : IEquatable<AssetReference>
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAssetSettings
|
||||
{
|
||||
ValueTask<Result<long>> WriteToStreamAsync(Stream stream, CancellationToken token = default);
|
||||
ValueTask<Result<IAssetSettings>> ReadFromStreamAsync(Stream stream, CancellationToken token = default);
|
||||
}
|
||||
public interface IAssetSettings;
|
||||
|
||||
@@ -11,29 +11,23 @@ public sealed class CustomAssetHandlerAttribute : Attribute
|
||||
get; init;
|
||||
}
|
||||
|
||||
public bool AllowCaching
|
||||
{
|
||||
get; init;
|
||||
} = true;
|
||||
|
||||
public required string[] SupportedExtensions
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DependencyUpdateType
|
||||
{
|
||||
Add,
|
||||
Remove
|
||||
public bool AllowCaching
|
||||
{
|
||||
get; init;
|
||||
} = true;
|
||||
}
|
||||
|
||||
public interface IAssetExportOptions;
|
||||
|
||||
public interface IAssetHandler
|
||||
{
|
||||
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default);
|
||||
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default);
|
||||
}
|
||||
|
||||
public interface IImportableAssetHandler : IAssetHandler
|
||||
|
||||
37
src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs
Normal file
37
src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class CustomAssetProcesserAttribute<T> : Attribute
|
||||
{
|
||||
public Type Type => typeof(T);
|
||||
}
|
||||
|
||||
public readonly struct AssetProcesserContext
|
||||
{
|
||||
public IAssetRegistry Registry
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public string AssetPath
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public Asset Asset
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public IAssetHandler Handler
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAssetProcesser
|
||||
{
|
||||
ValueTask ProcessAsync(AssetProcesserContext ctx);
|
||||
}
|
||||
@@ -41,13 +41,6 @@ public enum TextureCompressionLevel : uint
|
||||
High
|
||||
}
|
||||
|
||||
public enum TextureCompressionEffort : uint
|
||||
{
|
||||
Fastest,
|
||||
Normal,
|
||||
Production
|
||||
}
|
||||
|
||||
public enum MipmapFilter : uint
|
||||
{
|
||||
Box,
|
||||
@@ -59,14 +52,17 @@ public enum MipmapFilter : uint
|
||||
public class TextureAsset : Asset
|
||||
{
|
||||
internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F";
|
||||
|
||||
internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID);
|
||||
|
||||
public override Guid TypeID => s_typeGuid;
|
||||
private readonly Handle<Texture> _texture;
|
||||
|
||||
public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings)
|
||||
public override Guid TypeID => s_typeGuid;
|
||||
public Handle<Texture> Texture => _texture;
|
||||
|
||||
public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings, Handle<Texture> texture)
|
||||
: base(id, dependencies, settings)
|
||||
{
|
||||
_texture = texture;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,21 +138,16 @@ public class TextureAssetSettings : IAssetSettings
|
||||
get; set;
|
||||
} = TextureCompressionLevel.Normal;
|
||||
|
||||
public TextureCompressionEffort CompressionEffort
|
||||
{
|
||||
get; set;
|
||||
} = TextureCompressionEffort.Normal;
|
||||
|
||||
public bool UseBorderColor
|
||||
{
|
||||
get; set;
|
||||
} = false;
|
||||
|
||||
public Color32 BorderColor
|
||||
public Color128 BorderColor
|
||||
{
|
||||
get; set;
|
||||
} = new Color32(0, 0, 0, 0);
|
||||
|
||||
} = new Color128(0, 0, 0, 0);
|
||||
|
||||
public bool ZeroAlphaBorder
|
||||
{
|
||||
get; set;
|
||||
@@ -254,11 +245,12 @@ public class TextureAssetSettings : IAssetSettings
|
||||
|
||||
try
|
||||
{
|
||||
ref byte address = ref MemoryMarshal.GetReference(tempArray);
|
||||
await stream.ReadAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false);
|
||||
var basic = Unsafe.ReadUnaligned<BasicSettings>(ref address);
|
||||
var advanced = Unsafe.ReadUnaligned<AdvancedSettings>(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>()));
|
||||
var sampler = Unsafe.ReadUnaligned<SamplerSettings>(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()));
|
||||
|
||||
// Use index-based reads after the await to avoid 'ref across await' errors.
|
||||
var basic = Unsafe.ReadUnaligned<BasicSettings>(ref tempArray[0]);
|
||||
var advanced = Unsafe.ReadUnaligned<AdvancedSettings>(ref tempArray[Unsafe.SizeOf<BasicSettings>()]);
|
||||
var sampler = Unsafe.ReadUnaligned<SamplerSettings>(ref tempArray[Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()]);
|
||||
|
||||
var settings = new TextureAssetSettings
|
||||
{
|
||||
@@ -280,6 +272,7 @@ public class TextureAssetSettings : IAssetSettings
|
||||
}
|
||||
}
|
||||
|
||||
[CustomAssetHandler(ID = TextureAsset._TYPE_ID, SupportedExtensions = new[] { ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" })]
|
||||
internal class TextureAssetHandler : IImportableAssetHandler
|
||||
{
|
||||
private const int _CURRENT_VERSION = 1;
|
||||
@@ -291,87 +284,112 @@ 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)
|
||||
{
|
||||
return Result.Failure($"Unsupported image format with {info.BitsPerChannel} bits per channel.");
|
||||
}
|
||||
|
||||
ref byte pData = ref Unsafe.NullRef<byte>();
|
||||
var imageSize = 0ul;
|
||||
var isFloat = info.BitsPerChannel > 8;
|
||||
var width = info.Width;
|
||||
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, info.ColorComponents);
|
||||
pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan()));
|
||||
imageSize = image.Size;
|
||||
using var image = ImageResultFloat.FromStream(sourceStream, colorComponents);
|
||||
var span = MemoryMarshal.AsBytes(image.AsSpan());
|
||||
pixelBytes = new byte[span.Length];
|
||||
span.CopyTo(pixelBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var image = ImageResult.FromStream(sourceStream, info.ColorComponents);
|
||||
pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan()));
|
||||
imageSize = image.Size;
|
||||
using var image = ImageResult.FromStream(sourceStream, colorComponents);
|
||||
var span = MemoryMarshal.AsBytes(image.AsSpan());
|
||||
pixelBytes = new byte[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 -----------
|
||||
// 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
|
||||
var contentSize = _CONTENT_HEADER_SIZE + pixelBytes.Length;
|
||||
|
||||
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
|
||||
{
|
||||
HandlerVersion = _CURRENT_VERSION,
|
||||
SettingsOffset = AssetMetadata.SIZE,
|
||||
};
|
||||
|
||||
// Reserve space for the header, then write settings
|
||||
targetStream.Seek(0, SeekOrigin.Begin);
|
||||
AssetMetadata.WriteToStream(targetStream, ref header);
|
||||
|
||||
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
|
||||
var settings = new TextureAssetSettings();
|
||||
var sizeResult = await settings.WriteToStreamAsync(targetStream, token).ConfigureAwait(false);
|
||||
if (sizeResult.IsFailure)
|
||||
{
|
||||
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
|
||||
}
|
||||
|
||||
header.SettingsSize = sizeResult.Value;
|
||||
header.SettingsSize = sizeResult.Value;
|
||||
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
|
||||
header.ContentSize = (long)imageSize;
|
||||
header.ContentSize = contentSize;
|
||||
|
||||
// Write raw image content
|
||||
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
|
||||
|
||||
var offset = 0;
|
||||
var tempArray = ArrayPool<byte>.Shared.Rent((int)Math.Min(imageSize, 40960ul));
|
||||
var remaining = imageSize;
|
||||
|
||||
var contentHeader = ArrayPool<byte>.Shared.Rent(_CONTENT_HEADER_SIZE);
|
||||
try
|
||||
{
|
||||
while (remaining > 0)
|
||||
{
|
||||
var chunkSize = (int)Math.Min(remaining, (ulong)tempArray.Length);
|
||||
Unsafe.CopyBlockUnaligned(ref tempArray[0], ref Unsafe.Add(ref pData, offset), (uint)chunkSize);
|
||||
|
||||
await targetStream.WriteAsync(tempArray.AsMemory(0, chunkSize), token).ConfigureAwait(false);
|
||||
|
||||
offset += chunkSize;
|
||||
remaining -= (ulong)chunkSize;
|
||||
}
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to write texture asset content to stream: {ex.Message}");
|
||||
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(tempArray);
|
||||
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();
|
||||
}
|
||||
|
||||
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default)
|
||||
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default)
|
||||
public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
259
src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs
Normal file
259
src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using Ghost.Nvtt;
|
||||
using Ghost.Nvtt.Native;
|
||||
using Misaki.HighPerformance.Image;
|
||||
using System.IO.Hashing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Drives the NVTT compression + mipmap pipeline for a single texture asset.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// 1. Accept raw decoded pixel bytes + settings.
|
||||
/// 2. Determine the cache file path (<c>CachesFolderPath/TextureCache/<guid>_<hash>.dds</c>).
|
||||
/// 3. If the cache is already valid (hash matches), skip compression.
|
||||
/// 4. Otherwise run the full NVTT pipeline and write the DDS to the cache file.
|
||||
///
|
||||
/// The caller owns opening/closing all streams; this class only takes spans and paths.
|
||||
/// </summary>
|
||||
internal static unsafe class TextureProcessor
|
||||
{
|
||||
private const string _TEXTURE_CACHE_SUBFOLDER = "TextureCache";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public entry point
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Compresses <paramref name="pixelData"/> according to <paramref name="settings"/>
|
||||
/// and writes the result to the texture cache.
|
||||
///
|
||||
/// Returns the absolute path of the cache file on success.
|
||||
/// The cache file is skipped if it already exists with a matching content hash.
|
||||
/// </summary>
|
||||
public static string CompressToCache(
|
||||
string cachesFolderPath,
|
||||
Guid assetId,
|
||||
ReadOnlySpan<byte> pixelData,
|
||||
int width,
|
||||
int height,
|
||||
bool isFloat,
|
||||
ColorComponents colorComponents,
|
||||
TextureAssetSettings settings)
|
||||
{
|
||||
// --- derive cache path --------------------------------------------------
|
||||
var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER);
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
|
||||
var settingsHash = ComputeSettingsHash(settings);
|
||||
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;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// NVTT pipeline
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static void RunNvttPipeline(
|
||||
string outputPath,
|
||||
ReadOnlySpan<byte> pixelData,
|
||||
int width,
|
||||
int height,
|
||||
bool isFloat,
|
||||
ColorComponents colorComponents,
|
||||
TextureAssetSettings settings)
|
||||
{
|
||||
using var surface = new NvttSurfaceHandle();
|
||||
using var compOpts = new NvttCompressionOptionsHandle();
|
||||
using var outOpts = new NvttOutputOptionsHandle();
|
||||
using var ctx = new NvttContextHandle();
|
||||
|
||||
// ---- 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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// ---- 2. resize ---------------------------------------------------------
|
||||
var maxExtent = (int)settings.Sampler.MaxSize;
|
||||
if (settings.Advanced.StretchToPowerOfTwo)
|
||||
{
|
||||
surface.ResizeMakeSquare(maxExtent,
|
||||
NvttRoundMode.NVTT_RoundMode_ToPreviousPowerOfTwo,
|
||||
NvttResizeFilter.NVTT_ResizeFilter_Box);
|
||||
}
|
||||
else if (surface.Width > maxExtent || surface.Height > maxExtent)
|
||||
{
|
||||
surface.ResizeMax(maxExtent,
|
||||
NvttRoundMode.NVTT_RoundMode_None,
|
||||
NvttResizeFilter.NVTT_ResizeFilter_Box);
|
||||
}
|
||||
|
||||
// ---- 2b. border color --------------------------------------------------
|
||||
if (settings.Advanced.UseBorderColor)
|
||||
{
|
||||
var c = settings.Advanced.BorderColor;
|
||||
surface.SetBorder(c.r, c.g, c.b, c.a);
|
||||
}
|
||||
else if (settings.Advanced.ZeroAlphaBorder)
|
||||
{
|
||||
surface.SetBorder(0f, 0f, 0f, 0f);
|
||||
}
|
||||
|
||||
// ---- 3. colour-space: convert to linear before mip filtering -----------
|
||||
if (settings.Basic.IsSRGB && settings.Advanced.GammaCorrection)
|
||||
{
|
||||
surface.ToLinearFromSrgb();
|
||||
}
|
||||
|
||||
// ---- 4. premultiply alpha (before mip chain) ---------------------------
|
||||
if (settings.Advanced.PremultiplyAlpha)
|
||||
{
|
||||
surface.PremultiplyAlpha();
|
||||
}
|
||||
|
||||
// ---- 5. configure compression options ----------------------------------
|
||||
compOpts.Format = SelectFormat(settings);
|
||||
compOpts.Quality = SelectQuality(settings.Advanced.CompressionLevel);
|
||||
|
||||
if (settings.Advanced.CutoutAlpha)
|
||||
{
|
||||
compOpts.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;
|
||||
|
||||
// ---- 7. mipmap count ---------------------------------------------------
|
||||
var nvttFilter = SelectMipmapFilter(settings.Advanced.MipmapFilter);
|
||||
|
||||
int mipmapCount;
|
||||
if (!settings.Advanced.GenerateMipmaps)
|
||||
{
|
||||
mipmapCount = 1;
|
||||
}
|
||||
else if (settings.Advanced.MipmapLevelCount == 0)
|
||||
{
|
||||
mipmapCount = surface.CountMipmaps();
|
||||
}
|
||||
else
|
||||
{
|
||||
mipmapCount = (int)settings.Advanced.MipmapLevelCount;
|
||||
}
|
||||
|
||||
// ---- 8. enable CUDA if available ---------------------------------------
|
||||
ctx.SetCudaAcceleration(Ghost.Nvtt.NvttGlobal.IsCudaSupported);
|
||||
|
||||
// ---- 9. write DDS header -----------------------------------------------
|
||||
ctx.OutputHeader(surface, mipmapCount, compOpts, outOpts);
|
||||
|
||||
// ---- 10. compress mip chain using a working clone ----------------------
|
||||
using var mip = surface.Clone();
|
||||
|
||||
for (int level = 0; level < mipmapCount; level++)
|
||||
{
|
||||
// Scale alpha for coverage on each mip (if requested)
|
||||
if (settings.Advanced.ScaleAlphaForMipCoverage && level > 0)
|
||||
{
|
||||
float refCoverage = mip.AlphaTestCoverage(
|
||||
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f);
|
||||
mip.ScaleAlphaToCoverage(refCoverage,
|
||||
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f);
|
||||
}
|
||||
|
||||
ctx.Compress(mip, face: 0, mipmap: level, compOpts, outOpts);
|
||||
|
||||
if (level + 1 < mipmapCount)
|
||||
{
|
||||
mip.BuildNextMipmap(nvttFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static NvttFormat SelectFormat(TextureAssetSettings settings)
|
||||
=> settings.Basic.TextureType switch
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
private static NvttQuality SelectQuality(TextureCompressionLevel level)
|
||||
=> level switch
|
||||
{
|
||||
TextureCompressionLevel.Low => NvttQuality.NVTT_Quality_Fastest,
|
||||
TextureCompressionLevel.High => NvttQuality.NVTT_Quality_Production,
|
||||
_ => NvttQuality.NVTT_Quality_Normal,
|
||||
};
|
||||
|
||||
private static NvttMipmapFilter SelectMipmapFilter(MipmapFilter filter)
|
||||
=> filter switch
|
||||
{
|
||||
MipmapFilter.Box => NvttMipmapFilter.NVTT_MipmapFilter_Box,
|
||||
MipmapFilter.Triangle => NvttMipmapFilter.NVTT_MipmapFilter_Triangle,
|
||||
MipmapFilter.MitchellNetravali => NvttMipmapFilter.NVTT_MipmapFilter_Mitchell,
|
||||
_ => NvttMipmapFilter.NVTT_MipmapFilter_Kaiser,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Produces a stable 64-bit hash of the settings structs so the cache file
|
||||
/// name changes whenever any setting changes.
|
||||
/// </summary>
|
||||
private static ulong ComputeSettingsHash(TextureAssetSettings s)
|
||||
{
|
||||
var basicSize = Unsafe.SizeOf<TextureAssetSettings.BasicSettings>();
|
||||
var advancedSize = Unsafe.SizeOf<TextureAssetSettings.AdvancedSettings>();
|
||||
var samplerSize = Unsafe.SizeOf<TextureAssetSettings.SamplerSettings>();
|
||||
var total = basicSize + advancedSize + samplerSize;
|
||||
|
||||
Span<byte> buf = stackalloc byte[total];
|
||||
var basic = s.Basic;
|
||||
var advanced = s.Advanced;
|
||||
var sampler = s.Sampler;
|
||||
MemoryMarshal.Write(buf, in basic);
|
||||
MemoryMarshal.Write(buf.Slice(basicSize), in advanced);
|
||||
MemoryMarshal.Write(buf.Slice(basicSize + advancedSize), in sampler);
|
||||
|
||||
return XxHash64.HashToUInt64(buf);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
@@ -7,7 +6,10 @@ namespace Ghost.Editor.Core;
|
||||
public static class EditorApplication
|
||||
{
|
||||
public const string ASSETS_FOLDER_NAME = "Assets";
|
||||
public const string SOURCES_FOLDER_NAME = "Sources";
|
||||
public const string PACKAGES_FOLDER_NAME = "Packages";
|
||||
public const string CACHES_FOLDER_NAME = "Caches";
|
||||
public const string CONFIG_FOLDER_NAME = "Config";
|
||||
|
||||
private static IServiceProvider? s_serviceProvider;
|
||||
private static string s_currentProjectPath = string.Empty;
|
||||
@@ -16,8 +18,15 @@ public static class EditorApplication
|
||||
private static DispatcherQueue? s_dispatcherQueue;
|
||||
|
||||
internal static Application CurrentApplication => Application.Current;
|
||||
internal static string CurrentProjectPath => s_currentProjectPath;
|
||||
internal static string CurrentProjectName => s_currentProjectName;
|
||||
|
||||
public static string ProjectPath => s_currentProjectPath;
|
||||
public static string ProjectName => s_currentProjectName;
|
||||
|
||||
public static string AssetsFolderPath => Path.Combine(ProjectPath, ASSETS_FOLDER_NAME);
|
||||
public static string SourcesFolderPath => Path.Combine(ProjectPath, SOURCES_FOLDER_NAME);
|
||||
public static string PackagesFolderPath => Path.Combine(ProjectPath, PACKAGES_FOLDER_NAME);
|
||||
public static string CachesFolderPath => Path.Combine(ProjectPath, CACHES_FOLDER_NAME);
|
||||
public static string ConfigFolderPath => Path.Combine(ProjectPath, CONFIG_FOLDER_NAME);
|
||||
|
||||
public static DispatcherQueue DispatcherQueue
|
||||
{
|
||||
@@ -57,9 +66,5 @@ public static class EditorApplication
|
||||
|
||||
internal static void Shutdown()
|
||||
{
|
||||
if (s_serviceProvider?.GetService(typeof(IAssetService)) is AssetHandle.AssetService assetService)
|
||||
{
|
||||
assetService.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
|
||||
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -39,7 +39,7 @@ internal sealed partial class EngineEditorWindow : WindowEx
|
||||
|
||||
private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
PART_TitleBar.Title = EditorApplication.CurrentProjectName;
|
||||
PART_TitleBar.Title = EditorApplication.ProjectName;
|
||||
PART_TitleBar.Subtitle = $"Ghost Engine {Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}";
|
||||
|
||||
_notificationService.SetReference(InfoBar, NotificationQueue);
|
||||
|
||||
@@ -26,7 +26,7 @@ internal sealed partial class SplashWindow : WindowEx
|
||||
{
|
||||
var version = Package.Current.Id.Version;
|
||||
VersionTextBlock.Text = $"Version {version.Major}.{version.Minor}.{version.Build}";
|
||||
LoadingTextBlock.Text = $"Loading {EditorApplication.CurrentProjectName}...";
|
||||
LoadingTextBlock.Text = $"Loading {EditorApplication.ProjectName}...";
|
||||
CopyrightTextBlock.Text = $"Copyright © {DateTime.Now.Year} Ghost Engine. All rights reserved.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ internal partial class ProjectBrowserViewModel : ObservableObject
|
||||
_inspectorService = inspectorService;
|
||||
_assetService = assetService;
|
||||
|
||||
var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
LoadSubFolderRecursive(assetsRootItem);
|
||||
|
||||
Directories.Add(assetsRootItem);
|
||||
|
||||
@@ -41,7 +41,7 @@ internal partial class ProjectViewModel : ObservableObject
|
||||
{
|
||||
_assetService = assetService;
|
||||
|
||||
var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
LoadSubFolderRecursive(ref assetsRootItem);
|
||||
|
||||
SubDirectories.Add(assetsRootItem);
|
||||
|
||||
@@ -21,10 +21,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Misaki.HighPerformance" Version="1.0.4" />
|
||||
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.2.2" />
|
||||
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.3.3" />
|
||||
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.3.1" />
|
||||
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.3.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.1" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.1" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.3" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
|
||||
<PackageReference Include="ZLinq" Version="1.5.4" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using Ghost.Entities;
|
||||
using Ghost.Graphics;
|
||||
using Misaki.HighPerformance.Jobs;
|
||||
|
||||
namespace Ghost.Engine;
|
||||
|
||||
public interface IEngineContext : IDisposable
|
||||
{
|
||||
JobScheduler JobScheduler { get; }
|
||||
IJobScheduler JobScheduler { get; }
|
||||
IRenderSystem RenderSystem { get; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
@@ -17,13 +19,24 @@ internal class EngineEntryAttribute : Attribute
|
||||
internal sealed partial class EngineCore : IEngineContext
|
||||
{
|
||||
private readonly JobScheduler _jobScheduler;
|
||||
private readonly RenderSystem _renderSystem;
|
||||
|
||||
public JobScheduler JobScheduler => _jobScheduler;
|
||||
public IJobScheduler JobScheduler => _jobScheduler;
|
||||
public IRenderSystem RenderSystem => _renderSystem;
|
||||
|
||||
public EngineCore()
|
||||
{
|
||||
_jobScheduler = new JobScheduler(Environment.ProcessorCount - 2); // We -2 here, one for main thread, one for render thread
|
||||
|
||||
// TODO: Remove the windows dependency from RenderSystem.
|
||||
var renderingConfig = new RenderingConfig
|
||||
{
|
||||
FrameBufferCount = 2,
|
||||
GraphicsAPI = GraphicsAPI.Direct3D12,
|
||||
};
|
||||
|
||||
_renderSystem = new RenderSystem(renderingConfig);
|
||||
|
||||
ComponentRegistry.GetOrRegisterComponentID<ManagedEntityRef>();
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ public unsafe partial struct EntityQuery
|
||||
chunkInfos = chunkInfos.AsReadOnly()
|
||||
};
|
||||
|
||||
var handle = world.JobScheduler.ScheduleParallel(ref batchJob, chunkInfos.Count, batchSize, dependency);
|
||||
var handle = world.JobScheduler.ScheduleParallelFor(ref batchJob, chunkInfos.Count, batchSize, dependency);
|
||||
|
||||
var disposeJob = new DisposeJobChunk
|
||||
{
|
||||
|
||||
@@ -1178,7 +1178,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity1
|
||||
@@ -1334,7 +1334,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity2
|
||||
@@ -1517,7 +1517,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity3
|
||||
@@ -1727,7 +1727,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity4
|
||||
@@ -1964,7 +1964,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity5
|
||||
@@ -2228,7 +2228,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity6
|
||||
@@ -2519,7 +2519,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity7
|
||||
@@ -2837,7 +2837,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity8
|
||||
|
||||
@@ -216,7 +216,7 @@ public unsafe partial struct EntityQuery
|
||||
}
|
||||
}
|
||||
|
||||
var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency);
|
||||
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency);
|
||||
|
||||
// 3. Dispose the temp lists
|
||||
var disposeJob = new DisposeJobEntity<#= i #>
|
||||
|
||||
@@ -35,10 +35,9 @@ internal struct CBufferCache : IResourceReleasable
|
||||
}
|
||||
|
||||
_cpuData.Dispose();
|
||||
database.ScheduleReleaseResource(_gpuResource.AsResource());
|
||||
|
||||
database.ReleaseResource(GpuResource.AsResource());
|
||||
_gpuResource = Handle<GraphicsBuffer>.Invalid;
|
||||
|
||||
_size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +117,9 @@ public struct Mesh : IResourceReleasable
|
||||
{
|
||||
ReleaseCpuResources();
|
||||
|
||||
database.ReleaseResource(VertexBuffer.AsResource());
|
||||
database.ReleaseResource(IndexBuffer.AsResource());
|
||||
database.ReleaseResource(ObjectDataBuffer.AsResource());
|
||||
database.ScheduleReleaseResource(VertexBuffer.AsResource());
|
||||
database.ScheduleReleaseResource(IndexBuffer.AsResource());
|
||||
database.ScheduleReleaseResource(ObjectDataBuffer.AsResource());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
|
||||
_shaderCompiler = new DxcShaderCompiler();
|
||||
_descriptorAllocator = new D3D12DescriptorAllocator(_device);
|
||||
|
||||
_resourceDatabase = new D3D12ResourceDatabase(_descriptorAllocator);
|
||||
_resourceDatabase = new D3D12ResourceDatabase(renderSystem, _descriptorAllocator);
|
||||
_pipelineLibrary = new D3D12PipelineLibrary(_device, _resourceDatabase);
|
||||
_resourceAllocator = new D3D12ResourceAllocator(renderSystem, _device, _descriptorAllocator, _resourceDatabase, _pipelineLibrary);
|
||||
_resourceAllocator = new D3D12ResourceAllocator(_device, _descriptorAllocator, _resourceDatabase, _pipelineLibrary);
|
||||
|
||||
_renderers = ImmutableArray<IRenderer>.Empty;
|
||||
|
||||
@@ -127,7 +127,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
|
||||
}
|
||||
}
|
||||
|
||||
_resourceAllocator.ReleaseTempResources();
|
||||
_resourceDatabase.EndFrame();
|
||||
|
||||
return r;
|
||||
}
|
||||
@@ -144,6 +144,8 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
|
||||
renderer.Dispose();
|
||||
}
|
||||
|
||||
_resourceDatabase.ReleaseAllResourcesImmediately();
|
||||
|
||||
_resourceAllocator.Dispose();
|
||||
_pipelineLibrary.Dispose();
|
||||
_resourceDatabase.Dispose();
|
||||
|
||||
@@ -5,11 +5,8 @@ using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.D3D12.Utilities;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
@@ -454,21 +451,18 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
||||
|
||||
private UniquePtr<D3D12MA_Allocator> _d3d12MA;
|
||||
|
||||
private readonly IFenceSynchronizer _fenceSynchronizer;
|
||||
private readonly D3D12RenderDevice _device;
|
||||
private readonly D3D12DescriptorAllocator _descriptorAllocator;
|
||||
private readonly D3D12ResourceDatabase _resourceDatabase;
|
||||
private readonly D3D12PipelineLibrary _pipelineLibrary;
|
||||
|
||||
private UnsafeQueue<Handle<GPUResource>> _tempResources;
|
||||
|
||||
// TODO: We should use ring buffer pool in d3d12ma for upload buffer.
|
||||
private readonly Handle<GraphicsBuffer> _uploadBatch;
|
||||
private ulong _uploadBatchOffset;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public D3D12ResourceAllocator(
|
||||
IFenceSynchronizer fenceSynchronizer,
|
||||
D3D12RenderDevice device,
|
||||
D3D12DescriptorAllocator descriptorAllocator,
|
||||
D3D12ResourceDatabase resourceDatabase,
|
||||
@@ -485,14 +479,11 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
||||
ThrowIfFailed(D3D12MA_CreateAllocator(&desc, &pAllocator));
|
||||
_d3d12MA.Attach(pAllocator);
|
||||
|
||||
_fenceSynchronizer = fenceSynchronizer;
|
||||
_device = device;
|
||||
_descriptorAllocator = descriptorAllocator;
|
||||
_resourceDatabase = resourceDatabase;
|
||||
_pipelineLibrary = pipelineLibrary;
|
||||
|
||||
_tempResources = new UnsafeQueue<Handle<GPUResource>>(64, Allocator.Persistent);
|
||||
|
||||
// Create an upload batch
|
||||
var uploadDesc = new BufferDesc
|
||||
{
|
||||
@@ -513,13 +504,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Handle<GPUResource> TrackAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData barrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string name, bool isTemp)
|
||||
{
|
||||
var handle = _resourceDatabase.AddAllocation(allocation, _fenceSynchronizer.CPUFenceValue, barrierData, resourceDescriptor, desc, name);
|
||||
|
||||
if (isTemp)
|
||||
{
|
||||
_tempResources.Enqueue(handle);
|
||||
}
|
||||
|
||||
var handle = _resourceDatabase.AddAllocation(allocation, barrierData, resourceDescriptor, desc, name);
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -844,7 +829,10 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
||||
};
|
||||
|
||||
offset = 0;
|
||||
return CreateBuffer(in bufferDesc, "TempUploadBuffer", options);
|
||||
var handle = CreateBuffer(in bufferDesc, "TempUploadBuffer", options);
|
||||
|
||||
_resourceDatabase.ScheduleReleaseResource(handle.AsResource());
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -943,35 +931,6 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
||||
return _resourceDatabase.AddShader(shader);
|
||||
}
|
||||
|
||||
public void ReleaseTempResources()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
while (_tempResources.Count > 0)
|
||||
{
|
||||
var handle = _tempResources.Peek();
|
||||
var r = _resourceDatabase.GetResourceRecord(handle);
|
||||
if (r.IsFailure || !r.Value.Allocated)
|
||||
{
|
||||
// Resource already released or invalid, just dequeue
|
||||
_tempResources.Dequeue();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.Value.cpuFenceValue > _fenceSynchronizer.GPUFenceValue)
|
||||
{
|
||||
// Resource still in use by GPU, stop processing.
|
||||
// Since resources are enqueued in order, we can break here.
|
||||
break;
|
||||
}
|
||||
|
||||
_resourceDatabase.ReleaseResource(handle);
|
||||
_tempResources.Dequeue();
|
||||
}
|
||||
|
||||
_uploadBatchOffset = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
@@ -979,17 +938,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(_tempResources.Count == 0, "Temporary resources should be released before disposing the allocator.");
|
||||
|
||||
foreach (var handle in _tempResources)
|
||||
{
|
||||
_resourceDatabase.ReleaseResource(handle);
|
||||
}
|
||||
|
||||
_resourceDatabase.ReleaseResource(_uploadBatch.AsResource());
|
||||
|
||||
_resourceDatabase.ReleaseResourceImmediately(_uploadBatch.AsResource());
|
||||
_d3d12MA.Dispose();
|
||||
_tempResources.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
@@ -6,8 +6,6 @@ using Misaki.HighPerformance.Collections;
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
@@ -43,19 +41,17 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
|
||||
public ResourceBarrierData barrierData;
|
||||
|
||||
public uint cpuFenceValue;
|
||||
public readonly bool isExternal;
|
||||
|
||||
public readonly bool Allocated => isExternal ? resource.resource.Get() != null : resource.allocation.Get() != null;
|
||||
public readonly SharedPtr<ID3D12Resource> ResourcePtr => isExternal ? resource.resource.Get() : resource.allocation.Get()->GetResource();
|
||||
|
||||
public ResourceRecord(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceBarrierData barrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc)
|
||||
public ResourceRecord(D3D12MA_Allocation* allocation, ResourceBarrierData barrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc)
|
||||
{
|
||||
this.resource = new ResourceUnion(allocation);
|
||||
this.isExternal = false;
|
||||
|
||||
this.viewGroup = resourceDescriptor;
|
||||
this.cpuFenceValue = cpuFenceValue;
|
||||
this.barrierData = barrierData;
|
||||
this.desc = desc;
|
||||
}
|
||||
@@ -66,7 +62,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
this.isExternal = true;
|
||||
|
||||
this.viewGroup = viewGroup;
|
||||
this.cpuFenceValue = ~0u;
|
||||
this.barrierData = barrierData;
|
||||
this.desc = resource->GetDesc().ToResourceDesc();
|
||||
}
|
||||
@@ -91,6 +86,19 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct ReleaseEntry
|
||||
{
|
||||
public readonly ResourceRecord record;
|
||||
public readonly uint fenceValue;
|
||||
|
||||
public ReleaseEntry(ResourceRecord record, uint fenceValue)
|
||||
{
|
||||
this.record = record;
|
||||
this.fenceValue = fenceValue;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IFenceSynchronizer _fenceSynchronizer;
|
||||
private readonly D3D12DescriptorAllocator _descriptorAllocator;
|
||||
|
||||
private UnsafeSlotMap<ResourceRecord> _resources;
|
||||
@@ -103,10 +111,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
private UnsafeSlotMap<Material> _materials;
|
||||
private readonly DynamicArray<Shader> _shaders; // TODO: Use SlotMap?
|
||||
|
||||
private UnsafeQueue<ReleaseEntry> _releaseQueue;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public D3D12ResourceDatabase(D3D12DescriptorAllocator descriptorAllocator)
|
||||
public D3D12ResourceDatabase(IFenceSynchronizer fenceSynchronizer, D3D12DescriptorAllocator descriptorAllocator)
|
||||
{
|
||||
_fenceSynchronizer = fenceSynchronizer;
|
||||
_descriptorAllocator = descriptorAllocator;
|
||||
|
||||
_resources = new UnsafeSlotMap<ResourceRecord>(64, Allocator.Persistent, AllocationOption.Clear);
|
||||
@@ -117,6 +128,8 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
_meshes = new UnsafeSlotMap<Mesh>(64, Allocator.Persistent, AllocationOption.Clear);
|
||||
_materials = new UnsafeSlotMap<Material>(16, Allocator.Persistent, AllocationOption.Clear);
|
||||
_shaders = new DynamicArray<Shader>(16);
|
||||
|
||||
_releaseQueue = new UnsafeQueue<ReleaseEntry>(32, Allocator.Persistent);
|
||||
}
|
||||
|
||||
~D3D12ResourceDatabase()
|
||||
@@ -124,14 +137,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private void ReleaseResource<T>(ref T resource)
|
||||
private void ReleaseResource<T>(T resource)
|
||||
where T : IResourceReleasable
|
||||
{
|
||||
resource.ReleaseResource(this);
|
||||
resource = default!;
|
||||
}
|
||||
|
||||
public unsafe Handle<GPUResource> ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, string? name = null)
|
||||
internal unsafe Handle<GPUResource> ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, string? name = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
@@ -157,7 +169,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
return handle;
|
||||
}
|
||||
|
||||
public unsafe Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null)
|
||||
public unsafe Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (allocation == null)
|
||||
@@ -168,7 +180,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
return Handle<GPUResource>.Invalid;
|
||||
}
|
||||
|
||||
var id = _resources.Add(new ResourceRecord(allocation, cpuFenceValue, initialBarrierData, resourceDescriptor, desc), out var generation);
|
||||
var id = _resources.Add(new ResourceRecord(allocation, initialBarrierData, resourceDescriptor, desc), out var generation);
|
||||
var handle = new Handle<GPUResource>(id, generation);
|
||||
|
||||
#if DEBUG || GHOST_EDITOR
|
||||
@@ -281,16 +293,29 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
return null;
|
||||
}
|
||||
|
||||
// FIX: This should be queued to be released after GPU is done with it.
|
||||
public void ReleaseResource(Handle<GPUResource> handle)
|
||||
public void ScheduleReleaseResource(Handle<GPUResource> handle)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
if (!handle.IsValid)
|
||||
if (_resources.TryGetElementAt(handle.ID, handle.Generation, out var record))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new ReleaseEntry(record, _fenceSynchronizer.CPUFenceValue);
|
||||
|
||||
_releaseQueue.Enqueue(entry);
|
||||
_resources.Remove(handle.ID, handle.Generation);
|
||||
|
||||
#if DEBUG || GHOST_EDITOR
|
||||
_resourceName.Remove(handle, out var name);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void ReleaseResourceImmediately(Handle<GPUResource> handle)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
|
||||
if (!exist || !info.Allocated)
|
||||
{
|
||||
@@ -298,10 +323,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
}
|
||||
|
||||
info.Release(_descriptorAllocator);
|
||||
#if DEBUG || GHOST_EDITOR
|
||||
_resourceName.Remove(handle, out var name);
|
||||
#endif
|
||||
|
||||
_resources.Remove(handle.ID, handle.Generation);
|
||||
}
|
||||
|
||||
@@ -370,7 +391,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
return;
|
||||
}
|
||||
|
||||
ReleaseResource(ref mesh);
|
||||
ReleaseResource(mesh);
|
||||
_meshes.Remove(handle.ID, handle.Generation);
|
||||
}
|
||||
|
||||
@@ -409,7 +430,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
return;
|
||||
}
|
||||
|
||||
ReleaseResource(ref material);
|
||||
ReleaseResource(material);
|
||||
_materials.Remove(handle.ID, handle.Generation);
|
||||
}
|
||||
|
||||
@@ -448,49 +469,64 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
||||
}
|
||||
|
||||
ref var shader = ref _shaders[id.Value]!;
|
||||
ReleaseResource(ref shader);
|
||||
ReleaseResource(shader);
|
||||
}
|
||||
|
||||
public void EndFrame()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
while (_releaseQueue.Count > 0)
|
||||
{
|
||||
var toRelease = _releaseQueue.Peek();
|
||||
if (toRelease.fenceValue > _fenceSynchronizer.GPUFenceValue)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_releaseQueue.Dequeue();
|
||||
|
||||
toRelease.record.Release(_descriptorAllocator);
|
||||
}
|
||||
}
|
||||
|
||||
internal void ReleaseAllResourcesImmediately()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
foreach (var mesh in _meshes)
|
||||
{
|
||||
ReleaseResource(mesh);
|
||||
}
|
||||
|
||||
foreach (var material in _materials)
|
||||
{
|
||||
ReleaseResource(material);
|
||||
}
|
||||
|
||||
foreach (var shader in _shaders)
|
||||
{
|
||||
ReleaseResource(shader);
|
||||
}
|
||||
|
||||
foreach (ref var record in _resources)
|
||||
{
|
||||
record.Release(_descriptorAllocator);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
[DoesNotReturn]
|
||||
[Conditional("DEBUG")]
|
||||
static void ThrowMemoryLeakException(string resourceType, int count)
|
||||
{
|
||||
throw new MemoryLeakException($"ResourceAllocator is being disposed with {count} {resourceType} still registered. Ensure all resources are released before disposing.");
|
||||
}
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_resources.Count > 0)
|
||||
{
|
||||
ThrowMemoryLeakException("GPU resources", _resources.Count);
|
||||
}
|
||||
|
||||
if (_meshes.Count > 0)
|
||||
{
|
||||
ThrowMemoryLeakException("meshes", _meshes.Count);
|
||||
}
|
||||
|
||||
if (_materials.Count > 0)
|
||||
{
|
||||
ThrowMemoryLeakException("materials", _materials.Count);
|
||||
}
|
||||
|
||||
// DSL are reference space, it will be managed by GC, so we don't throw exception here.
|
||||
for (var i = 0; i < _shaders.Count; i++)
|
||||
{
|
||||
ref var shader = ref _shaders[i];
|
||||
ReleaseResource(ref shader);
|
||||
}
|
||||
|
||||
_resources.Dispose();
|
||||
_samplers.Dispose();
|
||||
_meshes.Dispose();
|
||||
_materials.Dispose();
|
||||
_releaseQueue.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ internal unsafe class D3D12SwapChain : ISwapChain
|
||||
// Release old back buffers and render targets
|
||||
for (var i = 0; i < _backBuffers.Count; i++)
|
||||
{
|
||||
_resourceDatabase.ReleaseResource(_backBuffers[i].AsResource());
|
||||
_resourceDatabase.ReleaseResourceImmediately(_backBuffers[i].AsResource());
|
||||
}
|
||||
|
||||
ThrowIfFailed(_swapChain.Get()->ResizeBuffers((uint)_backBuffers.Count, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, (uint)DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING));
|
||||
@@ -252,7 +252,7 @@ internal unsafe class D3D12SwapChain : ISwapChain
|
||||
|
||||
for (var i = 0; i < _backBuffers.Count; i++)
|
||||
{
|
||||
_resourceDatabase.ReleaseResource(_backBuffers[i].AsResource());
|
||||
_resourceDatabase.ScheduleReleaseResource(_backBuffers[i].AsResource());
|
||||
}
|
||||
|
||||
_backBuffers.Dispose();
|
||||
|
||||
@@ -7,11 +7,11 @@ namespace Ghost.Graphics.D3D12.Utilities;
|
||||
internal unsafe static class D3D12PipelineResource
|
||||
{
|
||||
private readonly static D3D12_INPUT_ELEMENT_DESC[] s_inputElementDescs = [
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Position.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 0u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Normal.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 16u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Tangent.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 32u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Uv.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 48u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Color.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 64u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Position.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 0u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Normal.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 16u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Tangent.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 32u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Uv.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 48u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Color.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 64u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 },
|
||||
];
|
||||
|
||||
public const DXGI_FORMAT SWAP_CHAIN_BACK_BUFFER_FORMAT = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
|
||||
@@ -17,15 +17,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="runtime\win-x64\native\dxcompiler.dll" />
|
||||
<None Remove="runtime\win-x64\native\dxil.dll" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="runtime\win-x64\native\dxcompiler.dll">
|
||||
<Content Include="runtimes\win-x64\native\dxcompiler.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="runtime\win-x64\native\dxil.dll">
|
||||
<Content Include="runtimes\win-x64\native\dxil.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ClassDiagram />
|
||||
@@ -96,10 +96,16 @@ public interface IResourceDatabase : IDisposable
|
||||
string? GetResourceName(Handle<GPUResource> handle);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a resource from the database using its handle.
|
||||
/// Releases the GPU resource associated with the specified handle, freeing any resources allocated to it.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle of the resource to be removed.</param>
|
||||
void ReleaseResource(Handle<GPUResource> handle);
|
||||
void ScheduleReleaseResource(Handle<GPUResource> handle);
|
||||
|
||||
/// <summary>
|
||||
/// Releases the GPU resource associated with the specified handle immediately, freeing any resources allocated to it.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle of the resource to be removed.</param>
|
||||
void ReleaseResourceImmediately(Handle<GPUResource> handle);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an existing sampler identifier that matches the specified description, or creates a new one if none
|
||||
|
||||
@@ -212,10 +212,10 @@ internal sealed class RenderGraphCompiler
|
||||
continue;
|
||||
}
|
||||
|
||||
_graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource);
|
||||
_graphicsEngine.ResourceDatabase.ScheduleReleaseResource(res.backingResource);
|
||||
}
|
||||
|
||||
_graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap);
|
||||
_graphicsEngine.ResourceDatabase.ScheduleReleaseResource(_resourceHeap);
|
||||
}
|
||||
|
||||
if (_aliasingManager.Heap.size == 0)
|
||||
@@ -380,11 +380,11 @@ internal sealed class RenderGraphCompiler
|
||||
{
|
||||
if (!res.isImported)
|
||||
{
|
||||
_graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource);
|
||||
_graphicsEngine.ResourceDatabase.ScheduleReleaseResource(res.backingResource);
|
||||
}
|
||||
}
|
||||
|
||||
_graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap);
|
||||
_graphicsEngine.ResourceDatabase.ScheduleReleaseResource(_resourceHeap);
|
||||
_resourceHeap = Handle<GPUResource>.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ internal class MeshRenderPass : IRenderPass
|
||||
{
|
||||
foreach (var texture in _textures)
|
||||
{
|
||||
resourceDatabase.ReleaseResource(texture.AsResource());
|
||||
resourceDatabase.ScheduleReleaseResource(texture.AsResource());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,6 @@ internal class RenderSystem : IRenderSystem
|
||||
// Sync the current frame resource to this new fence to keep state consistent
|
||||
frameResource.FenceValue = flushFence;
|
||||
|
||||
|
||||
foreach (var resource in _frameResources)
|
||||
{
|
||||
resource.CommandAllocator.Reset();
|
||||
|
||||
@@ -19,9 +19,9 @@ shader "Hidden/Blit"
|
||||
|
||||
includes
|
||||
{
|
||||
"F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.hlsl";
|
||||
"F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Color.hlsl";
|
||||
"F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Properties.hlsl";
|
||||
"F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Common.hlsl";
|
||||
"F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Color.hlsl";
|
||||
"F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl";
|
||||
}
|
||||
|
||||
hlsl
|
||||
|
||||
@@ -5,7 +5,6 @@ using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
|
||||
namespace Ghost.Entities.Test;
|
||||
|
||||
@@ -66,8 +65,6 @@ public class QueryBenchmark
|
||||
public void QueryEntities()
|
||||
{
|
||||
ref var query = ref _world.ComponentManager.GetEntityQueryReference(_queryIdentifier);
|
||||
var vecDT = Vector256.Create(_dt);
|
||||
|
||||
foreach (var chunkView in query.GetChunkIterator())
|
||||
{
|
||||
var positions = chunkView.GetComponentDataRW<Position>();
|
||||
|
||||
@@ -2,7 +2,6 @@ using Ghost.Core;
|
||||
using Ghost.Graphics.Test.Windows;
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
@@ -32,7 +31,7 @@ public partial class UnitTestApp : Application
|
||||
OperatingSystem.IsLinux() ? "linux" :
|
||||
OperatingSystem.IsMacOS() ? "osx" : "unknown";
|
||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
||||
var nativeDllDir = Path.Combine(currentDir, "runtime", platform + "-" + arch, "native");
|
||||
var nativeDllDir = Path.Combine(currentDir, "runtimes", platform + "-" + arch, "native");
|
||||
if (Directory.Exists(nativeDllDir))
|
||||
{
|
||||
foreach (var dll in Directory.EnumerateFiles(nativeDllDir, "*.dll"))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -13,6 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" />
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
206
src/Test/Ghost.MicroTest/NvttBindingTest.cs
Normal file
206
src/Test/Ghost.MicroTest/NvttBindingTest.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using Ghost.Nvtt;
|
||||
using Ghost.Nvtt.Native;
|
||||
using Ghost.Test.Core;
|
||||
|
||||
namespace Ghost.MicroTest;
|
||||
|
||||
/// <summary>
|
||||
/// Validates the NVTT binding + wrapper layer end-to-end.
|
||||
///
|
||||
/// Tests performed:
|
||||
/// 1. Version query — confirms the native DLL loads.
|
||||
/// 2. Surface load — loads an image from disk via NvttSurface.Load.
|
||||
/// 3. Resize — resizes to a power-of-two no larger than 512.
|
||||
/// 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.
|
||||
/// 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
|
||||
/// to the system temp folder.
|
||||
/// </summary>
|
||||
internal sealed unsafe class NvttBindingTest : ITest
|
||||
{
|
||||
private const string _IMAGE_PATH = @"C:\Users\Misaki\Downloads\Screenshot 2024-07-20 035047.png";
|
||||
|
||||
private string _outputDdsPath = string.Empty;
|
||||
|
||||
public void Setup()
|
||||
{
|
||||
_outputDdsPath = Path.Combine(Path.GetTempPath(), $"nvtt_test_{Guid.NewGuid():N}.dds");
|
||||
Console.WriteLine("[NvttBindingTest] Setup complete.");
|
||||
Console.WriteLine($"[NvttBindingTest] Input image : {_IMAGE_PATH}");
|
||||
Console.WriteLine($"[NvttBindingTest] Output DDS : {_outputDdsPath}");
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
// ---- Test 1: Version ---------------------------------------------------
|
||||
Console.Write("[Test 1] nvttVersion ... ");
|
||||
uint version = NvttGlobal.Version;
|
||||
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 ... ");
|
||||
bool cuda = NvttGlobal.IsCudaSupported;
|
||||
Console.WriteLine($"OK (cuda = {cuda})");
|
||||
|
||||
// ---- Test 3: Global message callback ----------------------------------
|
||||
Console.Write("[Test 3] SetMessageCallback ... ");
|
||||
int 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.
|
||||
}
|
||||
Console.WriteLine($"OK (no crash, callback fired {callbackFired} times during install)");
|
||||
|
||||
// ---- Test 4: Surface creation + load ----------------------------------
|
||||
Console.Write("[Test 4] NvttSurface.Load ... ");
|
||||
Assert(File.Exists(_IMAGE_PATH),
|
||||
$"Image not found: '{_IMAGE_PATH}'. Edit _IMAGE_PATH before running.");
|
||||
|
||||
using var surface = new NvttSurfaceHandle();
|
||||
bool loaded = surface.Load(_IMAGE_PATH, out bool hasAlpha);
|
||||
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})");
|
||||
|
||||
// ---- Test 5: Resize to power-of-two ≤ 512 ----------------------------
|
||||
Console.Write("[Test 5] ResizeMakeSquare ... ");
|
||||
surface.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})");
|
||||
|
||||
// ---- Test 6: sRGB → linear conversion ---------------------------------
|
||||
Console.Write("[Test 6] ToLinearFromSrgb ... ");
|
||||
surface.ToLinearFromSrgb(); // must not crash
|
||||
Console.WriteLine("OK");
|
||||
|
||||
// ---- Test 7: CountMipmaps ---------------------------------------------
|
||||
Console.Write("[Test 7] CountMipmaps ... ");
|
||||
int mipCount = surface.CountMipmaps();
|
||||
int expectedMax = (int)Math.Log2(Math.Max(surface.Width, surface.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 -------------------
|
||||
Console.Write("[Test 8] Compress BC7 in-memory ... ");
|
||||
long totalBytesReceived = 0;
|
||||
int imagesBegun = 0;
|
||||
|
||||
using var compOpts = new NvttCompressionOptionsHandle();
|
||||
compOpts.Format = NvttFormat.NVTT_Format_BC7;
|
||||
compOpts.Quality = NvttQuality.NVTT_Quality_Fastest;
|
||||
|
||||
using var outOpts = new NvttOutputOptionsHandle();
|
||||
outOpts.OutputHeader = true;
|
||||
outOpts.Srgb = true;
|
||||
outOpts.Container = NvttContainer.NVTT_Container_DDS10;
|
||||
|
||||
outOpts.SetOutputHandler(
|
||||
beginImage: (size, w, h, d, face, mip) =>
|
||||
{
|
||||
imagesBegun++;
|
||||
},
|
||||
outputData: (ptr, len) =>
|
||||
{
|
||||
totalBytesReceived += len;
|
||||
return true;
|
||||
},
|
||||
endImage: null
|
||||
);
|
||||
outOpts.SetErrorHandler(err =>
|
||||
Console.WriteLine($"\n [NVTT Error] {err}"));
|
||||
|
||||
using var ctx = new NvttContextHandle();
|
||||
ctx.SetCudaAcceleration(false); // CPU only for the test
|
||||
|
||||
using var mip = surface.Clone();
|
||||
bool headerOk = ctx.OutputHeader(mip, mipCount, compOpts, outOpts);
|
||||
Assert(headerOk, "OutputHeader returned false");
|
||||
|
||||
for (int level = 0; level < mipCount; level++)
|
||||
{
|
||||
bool compressOk = ctx.Compress(mip, face: 0, mipmap: level, compOpts, outOpts);
|
||||
Assert(compressOk, $"Compress returned false at mip level {level}");
|
||||
|
||||
if (level + 1 < mipCount)
|
||||
mip.BuildNextMipmap(NvttMipmapFilter.NVTT_MipmapFilter_Kaiser);
|
||||
}
|
||||
|
||||
Assert(imagesBegun == mipCount,
|
||||
$"Expected {mipCount} beginImage callbacks, got {imagesBegun}");
|
||||
Assert(totalBytesReceived > 0,
|
||||
$"No bytes received from output handler");
|
||||
Console.WriteLine($"OK ({imagesBegun} mips, {totalBytesReceived:N0} bytes total)");
|
||||
|
||||
// ---- Test 9: EstimateSize consistency ---------------------------------
|
||||
Console.Write("[Test 9] EstimateSize ... ");
|
||||
int estimated = ctx.EstimateSize(surface, mipCount, compOpts);
|
||||
// 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;
|
||||
|
||||
using var ctxFile = new NvttContextHandle();
|
||||
using var mipFile = surface.Clone();
|
||||
|
||||
bool fileHeaderOk = ctxFile.OutputHeader(mipFile, mipCount, compOpts, outOptsFile);
|
||||
Assert(fileHeaderOk, "File OutputHeader returned false");
|
||||
|
||||
for (int level = 0; level < mipCount; level++)
|
||||
{
|
||||
bool ok = ctxFile.Compress(mipFile, face: 0, mipmap: level, compOpts, outOptsFile);
|
||||
Assert(ok, $"File Compress returned false at level {level}");
|
||||
|
||||
if (level + 1 < mipCount)
|
||||
mipFile.BuildNextMipmap(NvttMipmapFilter.NVTT_MipmapFilter_Kaiser);
|
||||
}
|
||||
|
||||
Assert(File.Exists(_outputDdsPath),
|
||||
$"DDS output file was not created: {_outputDdsPath}");
|
||||
long 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.");
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
// Leave the DDS file in place so you can inspect it.
|
||||
Console.WriteLine($"\n[NvttBindingTest] Output DDS left at: {_outputDdsPath}");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static void Assert(bool condition, string message)
|
||||
{
|
||||
if (!condition)
|
||||
throw new InvalidOperationException($"[ASSERTION FAILED] {message}");
|
||||
}
|
||||
|
||||
private static bool IsPowerOfTwo(int n)
|
||||
=> n > 0 && (n & (n - 1)) == 0;
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
Console.WriteLine("Hello, World!");
|
||||
using Ghost.Test.Core;
|
||||
using Ghost.MicroTest;
|
||||
|
||||
TestRunner.Run<NvttBindingTest>();
|
||||
|
||||
@@ -15,17 +15,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="fmod.dll" />
|
||||
<None Remove="fmodstudio.dll" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="runtime\win-x64\native\fmod.dll">
|
||||
<None Update="runtimes\win-x64\native\fmod.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="runtime\win-x64\native\fmodstudio.dll">
|
||||
</None>
|
||||
<None Update="runtimes\win-x64\native\fmodstudio.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="runtime\win-x64\native\meshoptimizer.dll">
|
||||
<None Update="runtimes\win-x64\native\meshoptimizer.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="runtime\win-x64\native\nvtt.dll" />
|
||||
<Using Include="Ghost.Nvtt.Native" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="runtime\win-x64\native\nvtt.dll">
|
||||
<None Update="runtimes\win-x64\native\nvtt.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
212
src/ThridParty/Ghost.Nvtt/NVTT_USAGE.md
Normal file
212
src/ThridParty/Ghost.Nvtt/NVTT_USAGE.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Ghost.Nvtt – Usage Guide
|
||||
|
||||
`Ghost.Nvtt` is a managed C# wrapper over the NVIDIA Texture Tools 3 (nvtt) native library.
|
||||
All wrapper classes are in the `Ghost.Nvtt` namespace. Add a single `using Ghost.Nvtt;` and you have access to every wrapper class and every enum.
|
||||
|
||||
---
|
||||
|
||||
## Quick-start: compress a PNG to BC7 DDS
|
||||
|
||||
```csharp
|
||||
using Ghost.Nvtt;
|
||||
|
||||
// 1. Load source image
|
||||
using var surface = new NvttSurface();
|
||||
surface.Load("albedo.png", out bool hasAlpha);
|
||||
|
||||
// 2. Convert to linear before compression
|
||||
surface.ToLinearFromSrgb();
|
||||
|
||||
// 3. Build compression options
|
||||
using var compOpts = new NvttCompressionOptions();
|
||||
compOpts.SetFormat(NvttFormat.NVTT_Format_BC7);
|
||||
compOpts.SetQuality(NvttQuality.NVTT_Quality_Production);
|
||||
|
||||
// 4. Build output options (write to file)
|
||||
using var outOpts = new NvttOutputOptions();
|
||||
outOpts.SetFileName("albedo.dds");
|
||||
outOpts.SetOutputHeader(true);
|
||||
|
||||
// 5. Create context and compress the full mip chain
|
||||
using var ctx = new NvttContext();
|
||||
ctx.SetCudaAcceleration(NvttGlobal.IsCudaSupported);
|
||||
|
||||
int mipmaps = surface.CountMipmaps();
|
||||
ctx.OutputHeader(surface, mipmaps, compOpts, outOpts);
|
||||
|
||||
using var mip = surface.Clone();
|
||||
for (int m = 0; m < mipmaps; m++)
|
||||
{
|
||||
ctx.Compress(mip, 0, m, compOpts, outOpts);
|
||||
if (!mip.BuildNextMipmap(NvttMipmapFilter.NVTT_MipmapFilter_Box))
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Capturing compressed data in memory
|
||||
|
||||
Instead of writing to a file, provide output handlers to accumulate bytes:
|
||||
|
||||
```csharp
|
||||
using var outOpts = new NvttOutputOptions();
|
||||
var buffer = new List<byte>();
|
||||
|
||||
outOpts.SetOutputHandler(
|
||||
beginImage: (size, w, h, d, face, mip) => { /* optional: per-mip notification */ },
|
||||
outputData: (data, hasAlpha) => { buffer.AddRange(data); return true; },
|
||||
error: err => Console.Error.WriteLine(NvttGlobal.ErrorString(err)));
|
||||
outOpts.SetOutputHeader(true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cube maps
|
||||
|
||||
```csharp
|
||||
using var cube = new NvttCubeSurface();
|
||||
cube.Load("skybox.dds");
|
||||
|
||||
// Generate specular mip chain (cosine-power filter)
|
||||
using var filtered = cube.CosinePowerFilter(size: 128, cosinePower: 64f);
|
||||
|
||||
using var compOpts = new NvttCompressionOptions();
|
||||
compOpts.SetFormat(NvttFormat.NVTT_Format_BC6H_UF16);
|
||||
|
||||
using var outOpts = new NvttOutputOptions();
|
||||
outOpts.SetFileName("skybox_spec.dds");
|
||||
outOpts.SetOutputHeader(true);
|
||||
|
||||
using var ctx = new NvttContext();
|
||||
int mipmaps = filtered.MipmapCount;
|
||||
ctx.OutputHeaderCube(filtered, mipmaps, compOpts, outOpts);
|
||||
for (int m = 0; m < mipmaps; m++)
|
||||
ctx.CompressCube(filtered, m, compOpts, outOpts);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Loading an existing DDS (SurfaceSet)
|
||||
|
||||
`NvttSurfaceSet` reads DDS files that may contain multiple faces and mip levels
|
||||
without decoding them one-by-one:
|
||||
|
||||
```csharp
|
||||
using var set = new NvttSurfaceSet();
|
||||
set.LoadDDS("texture_array.dds");
|
||||
|
||||
Console.WriteLine($"{set.FaceCount} faces, {set.MipmapCount} mips, " +
|
||||
$"{set.Width}x{set.Height}");
|
||||
|
||||
// Access the raw pointer for face 0, mip 0 (borrowed – do not dispose)
|
||||
var surfacePtr = set.GetSurfacePtr(faceId: 0, mipId: 0);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Batch compression
|
||||
|
||||
Use `NvttBatchList` to compress many surfaces in a single driver call (better
|
||||
GPU utilisation):
|
||||
|
||||
```csharp
|
||||
using var batch = new NvttBatchList();
|
||||
using var compOpts = new NvttCompressionOptions();
|
||||
compOpts.SetFormat(NvttFormat.NVTT_Format_BC1);
|
||||
|
||||
// Build one NvttOutputOptions per destination
|
||||
var surfaces = LoadAllSurfaces(); // user-supplied IEnumerable<NvttSurface>
|
||||
var outOptsList = new List<NvttOutputOptions>();
|
||||
|
||||
foreach (var (surf, path) in surfaces.Zip(paths))
|
||||
{
|
||||
var oo = new NvttOutputOptions();
|
||||
oo.SetFileName(path);
|
||||
outOptsList.Add(oo);
|
||||
batch.Append(surf, face: 0, mipmap: 0, oo);
|
||||
}
|
||||
|
||||
using var ctx = new NvttContext();
|
||||
ctx.CompressBatch(batch, compOpts);
|
||||
|
||||
foreach (var oo in outOptsList) oo.Dispose();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timing
|
||||
|
||||
```csharp
|
||||
using var ctx = new NvttContext();
|
||||
ctx.EnableTiming(true, detailLevel: 1);
|
||||
|
||||
// ... compress ...
|
||||
|
||||
using var tc = new NvttTimingContext(detailLevel: 1);
|
||||
// OR use ctx.GetTimingContextPtr() to borrow the context's own timing data.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Global message callback
|
||||
|
||||
```csharp
|
||||
using var token = NvttGlobal.SetMessageCallback((severity, error, description) =>
|
||||
{
|
||||
if (severity == NvttSeverity.NVTT_Severity_Error)
|
||||
throw new Exception($"nvtt error {error}: {description}");
|
||||
Console.WriteLine($"[nvtt] {severity}: {description}");
|
||||
});
|
||||
|
||||
// ... do work ...
|
||||
|
||||
token.Dispose(); // unregisters the callback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Image comparison helpers
|
||||
|
||||
```csharp
|
||||
using var reference = new NvttSurface();
|
||||
reference.Load("original.png", out _);
|
||||
|
||||
using var compressed = new NvttSurface();
|
||||
compressed.Load("compressed.png", out _);
|
||||
|
||||
float rms = NvttGlobal.RmsError(reference, compressed);
|
||||
float cielab = NvttGlobal.RmsCIELabError(reference, compressed);
|
||||
|
||||
using var diff = NvttGlobal.Diff(reference, compressed, scale: 4f);
|
||||
diff.Save("diff.png");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common enums (all available without qualification after `using Ghost.Nvtt`)
|
||||
|
||||
| Enum | Key values |
|
||||
|------|-----------|
|
||||
| `NvttFormat` | `NVTT_Format_BC1` … `NVTT_Format_BC7`, `NVTT_Format_BC6H_UF16`, `NVTT_Format_RGBA` |
|
||||
| `NvttQuality` | `NVTT_Quality_Fastest`, `NVTT_Quality_Normal`, `NVTT_Quality_Production`, `NVTT_Quality_Highest` |
|
||||
| `NvttMipmapFilter` | `NVTT_MipmapFilter_Box`, `NVTT_MipmapFilter_Triangle`, `NVTT_MipmapFilter_Kaiser` |
|
||||
| `NvttResizeFilter` | `NVTT_ResizeFilter_Box`, `NVTT_ResizeFilter_Triangle`, `NVTT_ResizeFilter_Kaiser` |
|
||||
| `NvttRoundMode` | `NVTT_RoundMode_None`, `NVTT_RoundMode_ToPreviousPowerOfTwo`, `NVTT_RoundMode_ToNextPowerOfTwo` |
|
||||
| `NvttTextureType` | `NVTT_TextureType_2D`, `NVTT_TextureType_3D`, `NVTT_TextureType_Cube` |
|
||||
| `NvttCubeLayout` | `NVTT_CubeLayout_VerticalCross`, `NVTT_CubeLayout_HorizontalCross`, `NVTT_CubeLayout_Column` |
|
||||
| `EdgeFixup` | `NVTT_EdgeFixup_None`, `NVTT_EdgeFixup_Stretch`, `NVTT_EdgeFixup_Warp` |
|
||||
|
||||
---
|
||||
|
||||
## Ownership rules
|
||||
|
||||
| Returns | Ownership |
|
||||
|---------|-----------|
|
||||
| `new NvttSurface(...)` constructor overload accepting a raw pointer | **Takes** ownership – dispose when done |
|
||||
| `NvttSurface.Clone()` | Caller owns result |
|
||||
| `NvttSurface.CreateSubImage()`, `CreateToksvigMap()` | Caller owns result |
|
||||
| `NvttCubeSurface.Unfold()`, `IrradianceFilter()`, `CosinePowerFilter()`, `FastResample()` | Caller owns result |
|
||||
| `NvttGlobal.Diff()`, `Histogram()`, `HistogramRange()` | Caller owns result |
|
||||
| `NvttCubeSurface.FacePtr()`, `NvttSurfaceSet.GetSurfacePtr()` | **Borrowed** – do NOT dispose |
|
||||
| `NvttContext.GetTimingContextPtr()` | **Borrowed** – do NOT dispose |
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public static unsafe partial class Api
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum EdgeFixup
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
/// <summary>Defines the annotation found in a native declaration.</summary>
|
||||
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
/// <summary>Defines the type of a member as it was used in the native signature.</summary>
|
||||
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = false, Inherited = true)]
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttAlphaMode
|
||||
{
|
||||
6
src/ThridParty/Ghost.Nvtt/Native/NvttBatchList.cs
Normal file
6
src/ThridParty/Ghost.Nvtt/Native/NvttBatchList.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttBatchList
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttBoolean
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttCPUInputBuffer
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttChannelOrder
|
||||
{
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttCompressionOptions
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttContainer
|
||||
{
|
||||
6
src/ThridParty/Ghost.Nvtt/Native/NvttContext.cs
Normal file
6
src/ThridParty/Ghost.Nvtt/Native/NvttContext.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttCubeLayout
|
||||
{
|
||||
6
src/ThridParty/Ghost.Nvtt/Native/NvttCubeSurface.cs
Normal file
6
src/ThridParty/Ghost.Nvtt/Native/NvttCubeSurface.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttCubeSurface
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttEncodeFlags
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public unsafe partial struct NvttEncodeSettings
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttError
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttFormat
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttGPUInputBuffer
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttInputFormat
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttMipmapFilter
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttNormalTransform
|
||||
{
|
||||
6
src/ThridParty/Ghost.Nvtt/Native/NvttOutputOptions.cs
Normal file
6
src/ThridParty/Ghost.Nvtt/Native/NvttOutputOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttOutputOptions
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttPixelType
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttQuality
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public unsafe partial struct NvttRefImage
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttResizeFilter
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttRoundMode
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttSeverity
|
||||
{
|
||||
6
src/ThridParty/Ghost.Nvtt/Native/NvttSurface.cs
Normal file
6
src/ThridParty/Ghost.Nvtt/Native/NvttSurface.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttSurface
|
||||
{
|
||||
}
|
||||
}
|
||||
6
src/ThridParty/Ghost.Nvtt/Native/NvttSurfaceSet.cs
Normal file
6
src/ThridParty/Ghost.Nvtt/Native/NvttSurfaceSet.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttSurfaceSet
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttTextureType
|
||||
{
|
||||
6
src/ThridParty/Ghost.Nvtt/Native/NvttTimingContext.cs
Normal file
6
src/ThridParty/Ghost.Nvtt/Native/NvttTimingContext.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public partial struct NvttTimingContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttToneMapper
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttValueType
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt.Native
|
||||
{
|
||||
public enum NvttWrapMode
|
||||
{
|
||||
@@ -1,6 +1,91 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <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
|
||||
{
|
||||
public partial struct NvttBatchList
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,123 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <summary>
|
||||
/// Controls how a surface is compressed – format, quality, pixel layout and
|
||||
/// optional quantization settings.
|
||||
/// </summary>
|
||||
public sealed unsafe class NvttCompressionOptionsHandle : IDisposable
|
||||
{
|
||||
public partial struct NvttCompressionOptions
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,229 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper around the nvtt compression context — the central object that drives
|
||||
/// the compression pipeline.
|
||||
/// </summary>
|
||||
public sealed unsafe class NvttContextHandle : IDisposable
|
||||
{
|
||||
public partial struct NvttContext
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,227 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <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
|
||||
{
|
||||
public partial struct NvttCubeSurface
|
||||
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
|
||||
/// (0–5). 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
183
src/ThridParty/Ghost.Nvtt/NvttGlobal.cs
Normal file
183
src/ThridParty/Ghost.Nvtt/NvttGlobal.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
67
src/ThridParty/Ghost.Nvtt/NvttInterop.cs
Normal file
67
src/ThridParty/Ghost.Nvtt/NvttInterop.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,211 @@
|
||||
namespace Ghost.Nvtt
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <summary>
|
||||
/// Configures where compressed data is written and how it is formatted.
|
||||
///
|
||||
/// Managed callback delegates are stored in this object and passed to the
|
||||
/// native library via <see cref="Marshal.GetFunctionPointerForDelegate"/>.
|
||||
/// The delegates are kept alive by pinned <see cref="GCHandle"/> instances
|
||||
/// for the lifetime of this object.
|
||||
/// </summary>
|
||||
public sealed unsafe class NvttOutputOptionsHandle : IDisposable
|
||||
{
|
||||
public partial struct NvttOutputOptions
|
||||
private NvttOutputOptions* _ptr;
|
||||
|
||||
/// <summary>Raw pointer – use only when calling the native API directly.</summary>
|
||||
public NvttOutputOptions* Ptr => _ptr;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Managed callback storage
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Delegate types that match the native C signatures exactly.
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void BeginImageDelegate(int size, int width, int height, int depth, int face, int mipLevel);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate Ghost.Nvtt.Native.NvttBoolean OutputDataDelegate(void* data, int size, Ghost.Nvtt.Native.NvttBoolean lastChunk);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void ErrorDelegate(Ghost.Nvtt.Native.NvttError error);
|
||||
|
||||
// Pinned delegate instances – must stay alive as long as native code may call them.
|
||||
private BeginImageDelegate? _beginImageDelegate;
|
||||
private OutputDataDelegate? _outputDataDelegate;
|
||||
private ErrorDelegate? _errorDelegate;
|
||||
|
||||
// Managed user-facing callbacks.
|
||||
private Action<int, int, int, int, int, int>? _beginImage;
|
||||
private Func<nint, int, bool>? _outputData;
|
||||
private Action? _endImage;
|
||||
private Action<NvttError>? _errorHandler;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Construction / destruction
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public NvttOutputOptionsHandle() => _ptr = Api.nvttCreateOutputOptions();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_ptr != null)
|
||||
{
|
||||
Api.nvttDestroyOutputOptions(_ptr);
|
||||
_ptr = null;
|
||||
}
|
||||
// Release delegate references so GC can collect them.
|
||||
_beginImageDelegate = null;
|
||||
_outputDataDelegate = null;
|
||||
_errorDelegate = null;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Path of the output file. The file is created/overwritten when
|
||||
/// compression runs.
|
||||
/// </summary>
|
||||
public string FileName
|
||||
{
|
||||
set
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
Span<byte> buf = stackalloc byte[NvttInterop._MAX_STACK_PATH];
|
||||
var utf8 = NvttInterop.ToUtf8(value, buf);
|
||||
fixed (byte* p = utf8)
|
||||
{
|
||||
Api.nvttSetOutputOptionsFileName(_ptr, (sbyte*)p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether to write a DDS / KTX file header. Default is <c>true</c>.
|
||||
/// </summary>
|
||||
public bool OutputHeader
|
||||
{
|
||||
set { ThrowIfDisposed(); Api.nvttSetOutputOptionsOutputHeader(_ptr, NvttInterop.ToNvtt(value)); }
|
||||
}
|
||||
|
||||
/// <summary>Container format (DDS or DDS10).</summary>
|
||||
public NvttContainer Container
|
||||
{
|
||||
set { ThrowIfDisposed(); Api.nvttSetOutputOptionsContainer(_ptr, value); }
|
||||
}
|
||||
|
||||
/// <summary>Application-defined version number stored in the header.</summary>
|
||||
public int UserVersion
|
||||
{
|
||||
set { ThrowIfDisposed(); Api.nvttSetOutputOptionsUserVersion(_ptr, value); }
|
||||
}
|
||||
|
||||
/// <summary>Sets the sRGB flag in the output header.</summary>
|
||||
public bool Srgb
|
||||
{
|
||||
set { ThrowIfDisposed(); Api.nvttSetOutputOptionsSrgbFlag(_ptr, NvttInterop.ToNvtt(value)); }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Resets all options to their default values.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
Api.nvttResetOutputOptions(_ptr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs managed callbacks that receive the compressed data stream.
|
||||
///
|
||||
/// <para><paramref name="beginImage"/> is called once per mip level before any
|
||||
/// data arrives: <c>(size, width, height, depth, face, mipLevel)</c>.</para>
|
||||
/// <para><paramref name="outputData"/> receives each chunk of compressed bytes.
|
||||
/// The first argument is an <see cref="nint"/> pointing to the data, the
|
||||
/// second is the byte count. Return <c>true</c> to continue, <c>false</c>
|
||||
/// to abort.</para>
|
||||
/// <para><paramref name="endImage"/> is called once after the last chunk.</para>
|
||||
///
|
||||
/// Only one set of output callbacks can be active at a time; calling this
|
||||
/// method again replaces the previous ones.
|
||||
/// </summary>
|
||||
public void SetOutputHandler(
|
||||
Action<int, int, int, int, int, int>? beginImage,
|
||||
Func<nint, int, bool>? outputData,
|
||||
Action? endImage)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_beginImage = beginImage;
|
||||
_outputData = outputData;
|
||||
_endImage = endImage;
|
||||
RebindOutputHandler();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a managed error handler. The handler is invoked with the
|
||||
/// <see cref="NvttError"/> code whenever the native library encounters an
|
||||
/// error.
|
||||
/// </summary>
|
||||
public void SetErrorHandler(Action<NvttError>? handler)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_errorHandler = handler;
|
||||
RebindErrorHandler();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Internal delegate trampolines (instance-bound via closures)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void RebindOutputHandler()
|
||||
{
|
||||
// Capture current callbacks into local vars for the closure.
|
||||
var beginImage = _beginImage;
|
||||
var outputData = _outputData;
|
||||
|
||||
_beginImageDelegate = (size, width, height, depth, face, mipLevel) =>
|
||||
beginImage?.Invoke(size, width, height, depth, face, mipLevel);
|
||||
|
||||
_outputDataDelegate = (data, size, lastChunk) =>
|
||||
{
|
||||
bool ok = outputData?.Invoke((nint)data, size) ?? true;
|
||||
return ok ? Ghost.Nvtt.Native.NvttBoolean.NVTT_True
|
||||
: Ghost.Nvtt.Native.NvttBoolean.NVTT_False;
|
||||
};
|
||||
|
||||
nint beginPtr = Marshal.GetFunctionPointerForDelegate(_beginImageDelegate);
|
||||
nint outputPtr = Marshal.GetFunctionPointerForDelegate(_outputDataDelegate);
|
||||
|
||||
Api.nvttSetOutputOptionsOutputHandler(
|
||||
_ptr,
|
||||
(delegate* unmanaged[Cdecl]<int, int, int, int, int, int, void>)beginPtr,
|
||||
(delegate* unmanaged[Cdecl]<void*, int, Ghost.Nvtt.Native.NvttBoolean>)outputPtr,
|
||||
IntPtr.Zero);
|
||||
}
|
||||
|
||||
private void RebindErrorHandler()
|
||||
{
|
||||
var handler = _errorHandler;
|
||||
_errorDelegate = error => handler?.Invoke(error);
|
||||
|
||||
nint errorPtr = Marshal.GetFunctionPointerForDelegate(_errorDelegate);
|
||||
Api.nvttSetOutputOptionsErrorHandler(
|
||||
_ptr,
|
||||
(delegate* unmanaged[Cdecl]<Ghost.Nvtt.Native.NvttError, void>)errorPtr);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_ptr == null)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(NvttOutputOptionsHandle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,715 @@
|
||||
namespace Ghost.Nvtt
|
||||
using Ghost.Nvtt.Native;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <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
|
||||
{
|
||||
public partial struct NvttSurface
|
||||
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();
|
||||
float* p = Api.nvttSurfaceData(_ptr);
|
||||
int 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();
|
||||
float* p = Api.nvttSurfaceChannel(_ptr, index);
|
||||
int 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)
|
||||
{
|
||||
Ghost.Nvtt.Native.NvttBoolean nvAlpha;
|
||||
bool 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)
|
||||
{
|
||||
Ghost.Nvtt.Native.NvttBoolean nvAlpha;
|
||||
bool 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();
|
||||
float* 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,144 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <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
|
||||
{
|
||||
public partial struct NvttSurfaceSet
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,99 @@
|
||||
namespace Ghost.Nvtt
|
||||
namespace Ghost.Nvtt;
|
||||
|
||||
/// <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
|
||||
{
|
||||
public partial struct NvttTimingContext
|
||||
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);
|
||||
string 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user