Add Ghost.Nvtt C# wrapper and integrate nvtt texture pipeline

- Introduce full managed C# wrapper for NVIDIA Texture Tools (nvtt) with safe handle classes, idiomatic APIs, and managed callback support.
- Integrate Ghost.Nvtt into Ghost.Editor.Core and Ghost.MicroTest; update TextureAssetHandler to use the new nvtt wrapper for texture compression.
- Add comprehensive end-to-end binding test (NvttBindingTest).
- Refactor D3D12 resource management: add deferred/immediate release APIs, update allocator/database usage, and ensure proper resource cleanup.
- Update project files for new native DLL layout and dependency versions.
- Minor API cleanups: EditorApplication properties, D3D12 input layout, and removal of obsolete code.
- Update shaders, tests, and documentation for new APIs and usage patterns.
This commit is contained in:
2026-02-23 17:13:10 +09:00
parent 78e3b4ef31
commit 93c58fa7fb
91 changed files with 3124 additions and 313 deletions

View File

@@ -1,8 +1,6 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Serialization;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.AssetHandler;
@@ -178,8 +176,4 @@ public readonly struct AssetReference : IEquatable<AssetReference>
} }
} }
public interface IAssetSettings public interface IAssetSettings;
{
ValueTask<Result<long>> WriteToStreamAsync(Stream stream, CancellationToken token = default);
ValueTask<Result<IAssetSettings>> ReadFromStreamAsync(Stream stream, CancellationToken token = default);
}

View File

@@ -11,29 +11,23 @@ public sealed class CustomAssetHandlerAttribute : Attribute
get; init; get; init;
} }
public bool AllowCaching
{
get; init;
} = true;
public required string[] SupportedExtensions public required string[] SupportedExtensions
{ {
get; init; get; init;
} }
}
public enum DependencyUpdateType public bool AllowCaching
{ {
Add, get; init;
Remove } = true;
} }
public interface IAssetExportOptions; public interface IAssetExportOptions;
public interface IAssetHandler public interface IAssetHandler
{ {
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default); ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default);
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default); ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default);
} }
public interface IImportableAssetHandler : IAssetHandler public interface IImportableAssetHandler : IAssetHandler

View 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);
}

View File

@@ -41,13 +41,6 @@ public enum TextureCompressionLevel : uint
High High
} }
public enum TextureCompressionEffort : uint
{
Fastest,
Normal,
Production
}
public enum MipmapFilter : uint public enum MipmapFilter : uint
{ {
Box, Box,
@@ -59,14 +52,17 @@ public enum MipmapFilter : uint
public class TextureAsset : Asset public class TextureAsset : Asset
{ {
internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F"; internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F";
internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID); 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) : base(id, dependencies, settings)
{ {
_texture = texture;
} }
} }
@@ -142,20 +138,15 @@ public class TextureAssetSettings : IAssetSettings
get; set; get; set;
} = TextureCompressionLevel.Normal; } = TextureCompressionLevel.Normal;
public TextureCompressionEffort CompressionEffort
{
get; set;
} = TextureCompressionEffort.Normal;
public bool UseBorderColor public bool UseBorderColor
{ {
get; set; get; set;
} = false; } = false;
public Color32 BorderColor public Color128 BorderColor
{ {
get; set; get; set;
} = new Color32(0, 0, 0, 0); } = new Color128(0, 0, 0, 0);
public bool ZeroAlphaBorder public bool ZeroAlphaBorder
{ {
@@ -254,11 +245,12 @@ public class TextureAssetSettings : IAssetSettings
try try
{ {
ref byte address = ref MemoryMarshal.GetReference(tempArray);
await stream.ReadAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false); 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>())); // Use index-based reads after the await to avoid 'ref across await' errors.
var sampler = Unsafe.ReadUnaligned<SamplerSettings>(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>())); 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 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 internal class TextureAssetHandler : IImportableAssetHandler
{ {
private const int _CURRENT_VERSION = 1; 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) public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default)
{ {
// ---- 1. Probe image info -----------------------------------------------
var info = ImageInfo.FromStream(sourceStream); var info = ImageInfo.FromStream(sourceStream);
if (info.BitsPerChannel <= 0) if (info.BitsPerChannel <= 0)
{
return Result.Failure($"Unsupported image format with {info.BitsPerChannel} bits per channel."); 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 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) if (isFloat)
{ {
using var image = ImageResultFloat.FromStream(sourceStream, info.ColorComponents); using var image = ImageResultFloat.FromStream(sourceStream, colorComponents);
pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan())); var span = MemoryMarshal.AsBytes(image.AsSpan());
imageSize = image.Size; pixelBytes = new byte[span.Length];
span.CopyTo(pixelBytes);
} }
else else
{ {
using var image = ImageResult.FromStream(sourceStream, info.ColorComponents); using var image = ImageResult.FromStream(sourceStream, colorComponents);
pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan())); var span = MemoryMarshal.AsBytes(image.AsSpan());
imageSize = image.Size; 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) var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{ {
HandlerVersion = _CURRENT_VERSION, HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE, SettingsOffset = AssetMetadata.SIZE,
}; };
// Reserve space for the header, then write settings
targetStream.Seek(0, SeekOrigin.Begin); targetStream.Seek(0, SeekOrigin.Begin);
AssetMetadata.WriteToStream(targetStream, ref header); AssetMetadata.WriteToStream(targetStream, ref header);
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin); targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
var settings = new TextureAssetSettings();
var sizeResult = await settings.WriteToStreamAsync(targetStream, token).ConfigureAwait(false); var sizeResult = await settings.WriteToStreamAsync(targetStream, token).ConfigureAwait(false);
if (sizeResult.IsFailure) if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}"); 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.ContentOffset = header.SettingsOffset + sizeResult.Value;
header.ContentSize = (long)imageSize; header.ContentSize = contentSize;
// Write raw image content
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin); targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var offset = 0; var contentHeader = ArrayPool<byte>.Shared.Rent(_CONTENT_HEADER_SIZE);
var tempArray = ArrayPool<byte>.Shared.Rent((int)Math.Min(imageSize, 40960ul));
var remaining = imageSize;
try try
{ {
while (remaining > 0) BitConverter.TryWriteBytes(contentHeader.AsSpan(0, 4), width);
{ BitConverter.TryWriteBytes(contentHeader.AsSpan(4, 4), height);
var chunkSize = (int)Math.Min(remaining, (ulong)tempArray.Length); contentHeader[8] = isFloat ? (byte)1 : (byte)0;
Unsafe.CopyBlockUnaligned(ref tempArray[0], ref Unsafe.Add(ref pData, offset), (uint)chunkSize); BitConverter.TryWriteBytes(contentHeader.AsSpan(9, 4), (int)colorComponents);
await targetStream.WriteAsync(contentHeader.AsMemory(0, _CONTENT_HEADER_SIZE), token).ConfigureAwait(false);
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}");
} }
finally finally
{ {
ArrayPool<byte>.Shared.Return(tempArray); ArrayPool<byte>.Shared.Return(contentHeader);
}
} }
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default) await targetStream.WriteAsync(pixelBytes, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
// Patch header now that all sizes are known
targetStream.Seek(0, SeekOrigin.Begin);
AssetMetadata.WriteToStream(targetStream, ref header);
return Result.Success();
}
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)
{ {
throw new NotImplementedException(); 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(); throw new NotImplementedException();
} }

View 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/&lt;guid&gt;_&lt;hash&gt;.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);
}
}

View File

@@ -1,4 +1,3 @@
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -7,7 +6,10 @@ namespace Ghost.Editor.Core;
public static class EditorApplication public static class EditorApplication
{ {
public const string ASSETS_FOLDER_NAME = "Assets"; 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 CACHES_FOLDER_NAME = "Caches";
public const string CONFIG_FOLDER_NAME = "Config";
private static IServiceProvider? s_serviceProvider; private static IServiceProvider? s_serviceProvider;
private static string s_currentProjectPath = string.Empty; private static string s_currentProjectPath = string.Empty;
@@ -16,8 +18,15 @@ public static class EditorApplication
private static DispatcherQueue? s_dispatcherQueue; private static DispatcherQueue? s_dispatcherQueue;
internal static Application CurrentApplication => Application.Current; 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 public static DispatcherQueue DispatcherQueue
{ {
@@ -57,9 +66,5 @@ public static class EditorApplication
internal static void Shutdown() internal static void Shutdown()
{ {
if (s_serviceProvider?.GetService(typeof(IAssetService)) is AssetHandle.AssetService assetService)
{
assetService.Shutdown();
}
} }
} }

View File

@@ -23,6 +23,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -39,7 +39,7 @@ internal sealed partial class EngineEditorWindow : WindowEx
private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) 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}"; PART_TitleBar.Subtitle = $"Ghost Engine {Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}";
_notificationService.SetReference(InfoBar, NotificationQueue); _notificationService.SetReference(InfoBar, NotificationQueue);

View File

@@ -26,7 +26,7 @@ internal sealed partial class SplashWindow : WindowEx
{ {
var version = Package.Current.Id.Version; var version = Package.Current.Id.Version;
VersionTextBlock.Text = $"Version {version.Major}.{version.Minor}.{version.Build}"; 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."; CopyrightTextBlock.Text = $"Copyright © {DateTime.Now.Year} Ghost Engine. All rights reserved.";
} }
} }

View File

@@ -46,7 +46,7 @@ internal partial class ProjectBrowserViewModel : ObservableObject
_inspectorService = inspectorService; _inspectorService = inspectorService;
_assetService = assetService; _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); LoadSubFolderRecursive(assetsRootItem);
Directories.Add(assetsRootItem); Directories.Add(assetsRootItem);

View File

@@ -41,7 +41,7 @@ internal partial class ProjectViewModel : ObservableObject
{ {
_assetService = assetService; _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); LoadSubFolderRecursive(ref assetsRootItem);
SubDirectories.Add(assetsRootItem); SubDirectories.Add(assetsRootItem);

View File

@@ -21,10 +21,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.4" /> <PackageReference Include="Misaki.HighPerformance" Version="1.0.4" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.2.2" /> <PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.3.1" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.3.3" /> <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="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="TerraFX.Interop.Windows" Version="10.0.26100.6" />
<PackageReference Include="ZLinq" Version="1.5.4" /> <PackageReference Include="ZLinq" Version="1.5.4" />
</ItemGroup> </ItemGroup>

View File

@@ -1,11 +1,13 @@
using Ghost.Entities; using Ghost.Entities;
using Ghost.Graphics;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
namespace Ghost.Engine; namespace Ghost.Engine;
public interface IEngineContext : IDisposable public interface IEngineContext : IDisposable
{ {
JobScheduler JobScheduler { get; } IJobScheduler JobScheduler { get; }
IRenderSystem RenderSystem { get; }
} }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
@@ -17,13 +19,24 @@ internal class EngineEntryAttribute : Attribute
internal sealed partial class EngineCore : IEngineContext internal sealed partial class EngineCore : IEngineContext
{ {
private readonly JobScheduler _jobScheduler; private readonly JobScheduler _jobScheduler;
private readonly RenderSystem _renderSystem;
public JobScheduler JobScheduler => _jobScheduler; public IJobScheduler JobScheduler => _jobScheduler;
public IRenderSystem RenderSystem => _renderSystem;
public EngineCore() public EngineCore()
{ {
_jobScheduler = new JobScheduler(Environment.ProcessorCount - 2); // We -2 here, one for main thread, one for render thread _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>(); ComponentRegistry.GetOrRegisterComponentID<ManagedEntityRef>();
} }

View File

@@ -82,7 +82,7 @@ public unsafe partial struct EntityQuery
chunkInfos = chunkInfos.AsReadOnly() 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 var disposeJob = new DisposeJobChunk
{ {

View File

@@ -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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity1 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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity2 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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity3 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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity4 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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity5 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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity6 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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity7 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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity8 var disposeJob = new DisposeJobEntity8

View File

@@ -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 // 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity<#= i #> var disposeJob = new DisposeJobEntity<#= i #>

View File

@@ -35,10 +35,9 @@ internal struct CBufferCache : IResourceReleasable
} }
_cpuData.Dispose(); _cpuData.Dispose();
database.ScheduleReleaseResource(_gpuResource.AsResource());
database.ReleaseResource(GpuResource.AsResource());
_gpuResource = Handle<GraphicsBuffer>.Invalid; _gpuResource = Handle<GraphicsBuffer>.Invalid;
_size = 0; _size = 0;
} }
} }

View File

@@ -117,9 +117,9 @@ public struct Mesh : IResourceReleasable
{ {
ReleaseCpuResources(); ReleaseCpuResources();
database.ReleaseResource(VertexBuffer.AsResource()); database.ScheduleReleaseResource(VertexBuffer.AsResource());
database.ReleaseResource(IndexBuffer.AsResource()); database.ScheduleReleaseResource(IndexBuffer.AsResource());
database.ReleaseResource(ObjectDataBuffer.AsResource()); database.ScheduleReleaseResource(ObjectDataBuffer.AsResource());
} }
} }

View File

@@ -47,9 +47,9 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
_shaderCompiler = new DxcShaderCompiler(); _shaderCompiler = new DxcShaderCompiler();
_descriptorAllocator = new D3D12DescriptorAllocator(_device); _descriptorAllocator = new D3D12DescriptorAllocator(_device);
_resourceDatabase = new D3D12ResourceDatabase(_descriptorAllocator); _resourceDatabase = new D3D12ResourceDatabase(renderSystem, _descriptorAllocator);
_pipelineLibrary = new D3D12PipelineLibrary(_device, _resourceDatabase); _pipelineLibrary = new D3D12PipelineLibrary(_device, _resourceDatabase);
_resourceAllocator = new D3D12ResourceAllocator(renderSystem, _device, _descriptorAllocator, _resourceDatabase, _pipelineLibrary); _resourceAllocator = new D3D12ResourceAllocator(_device, _descriptorAllocator, _resourceDatabase, _pipelineLibrary);
_renderers = ImmutableArray<IRenderer>.Empty; _renderers = ImmutableArray<IRenderer>.Empty;
@@ -127,7 +127,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
} }
} }
_resourceAllocator.ReleaseTempResources(); _resourceDatabase.EndFrame();
return r; return r;
} }
@@ -144,6 +144,8 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
renderer.Dispose(); renderer.Dispose();
} }
_resourceDatabase.ReleaseAllResourcesImmediately();
_resourceAllocator.Dispose(); _resourceAllocator.Dispose();
_pipelineLibrary.Dispose(); _pipelineLibrary.Dispose();
_resourceDatabase.Dispose(); _resourceDatabase.Dispose();

View File

@@ -5,11 +5,8 @@ using Ghost.Graphics.Core;
using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows; using TerraFX.Interop.Windows;
@@ -454,21 +451,18 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
private UniquePtr<D3D12MA_Allocator> _d3d12MA; private UniquePtr<D3D12MA_Allocator> _d3d12MA;
private readonly IFenceSynchronizer _fenceSynchronizer;
private readonly D3D12RenderDevice _device; private readonly D3D12RenderDevice _device;
private readonly D3D12DescriptorAllocator _descriptorAllocator; private readonly D3D12DescriptorAllocator _descriptorAllocator;
private readonly D3D12ResourceDatabase _resourceDatabase; private readonly D3D12ResourceDatabase _resourceDatabase;
private readonly D3D12PipelineLibrary _pipelineLibrary; 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 readonly Handle<GraphicsBuffer> _uploadBatch;
private ulong _uploadBatchOffset; private ulong _uploadBatchOffset;
private bool _disposed; private bool _disposed;
public D3D12ResourceAllocator( public D3D12ResourceAllocator(
IFenceSynchronizer fenceSynchronizer,
D3D12RenderDevice device, D3D12RenderDevice device,
D3D12DescriptorAllocator descriptorAllocator, D3D12DescriptorAllocator descriptorAllocator,
D3D12ResourceDatabase resourceDatabase, D3D12ResourceDatabase resourceDatabase,
@@ -485,14 +479,11 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
ThrowIfFailed(D3D12MA_CreateAllocator(&desc, &pAllocator)); ThrowIfFailed(D3D12MA_CreateAllocator(&desc, &pAllocator));
_d3d12MA.Attach(pAllocator); _d3d12MA.Attach(pAllocator);
_fenceSynchronizer = fenceSynchronizer;
_device = device; _device = device;
_descriptorAllocator = descriptorAllocator; _descriptorAllocator = descriptorAllocator;
_resourceDatabase = resourceDatabase; _resourceDatabase = resourceDatabase;
_pipelineLibrary = pipelineLibrary; _pipelineLibrary = pipelineLibrary;
_tempResources = new UnsafeQueue<Handle<GPUResource>>(64, Allocator.Persistent);
// Create an upload batch // Create an upload batch
var uploadDesc = new BufferDesc var uploadDesc = new BufferDesc
{ {
@@ -513,13 +504,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private Handle<GPUResource> TrackAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData barrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string name, bool isTemp) 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); var handle = _resourceDatabase.AddAllocation(allocation, barrierData, resourceDescriptor, desc, name);
if (isTemp)
{
_tempResources.Enqueue(handle);
}
return handle; return handle;
} }
@@ -844,7 +829,10 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
}; };
offset = 0; 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); 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() public void Dispose()
{ {
if (_disposed) if (_disposed)
@@ -979,17 +938,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
return; return;
} }
Debug.Assert(_tempResources.Count == 0, "Temporary resources should be released before disposing the allocator."); _resourceDatabase.ReleaseResourceImmediately(_uploadBatch.AsResource());
foreach (var handle in _tempResources)
{
_resourceDatabase.ReleaseResource(handle);
}
_resourceDatabase.ReleaseResource(_uploadBatch.AsResource());
_d3d12MA.Dispose(); _d3d12MA.Dispose();
_tempResources.Dispose();
_disposed = true; _disposed = true;
GC.SuppressFinalize(this); GC.SuppressFinalize(this);

View File

@@ -6,8 +6,6 @@ using Misaki.HighPerformance.Collections;
using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
@@ -43,19 +41,17 @@ internal class D3D12ResourceDatabase : IResourceDatabase
public ResourceBarrierData barrierData; public ResourceBarrierData barrierData;
public uint cpuFenceValue;
public readonly bool isExternal; public readonly bool isExternal;
public readonly bool Allocated => isExternal ? resource.resource.Get() != null : resource.allocation.Get() != null; 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 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.resource = new ResourceUnion(allocation);
this.isExternal = false; this.isExternal = false;
this.viewGroup = resourceDescriptor; this.viewGroup = resourceDescriptor;
this.cpuFenceValue = cpuFenceValue;
this.barrierData = barrierData; this.barrierData = barrierData;
this.desc = desc; this.desc = desc;
} }
@@ -66,7 +62,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
this.isExternal = true; this.isExternal = true;
this.viewGroup = viewGroup; this.viewGroup = viewGroup;
this.cpuFenceValue = ~0u;
this.barrierData = barrierData; this.barrierData = barrierData;
this.desc = resource->GetDesc().ToResourceDesc(); 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 readonly D3D12DescriptorAllocator _descriptorAllocator;
private UnsafeSlotMap<ResourceRecord> _resources; private UnsafeSlotMap<ResourceRecord> _resources;
@@ -103,10 +111,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase
private UnsafeSlotMap<Material> _materials; private UnsafeSlotMap<Material> _materials;
private readonly DynamicArray<Shader> _shaders; // TODO: Use SlotMap? private readonly DynamicArray<Shader> _shaders; // TODO: Use SlotMap?
private UnsafeQueue<ReleaseEntry> _releaseQueue;
private bool _disposed; private bool _disposed;
public D3D12ResourceDatabase(D3D12DescriptorAllocator descriptorAllocator) public D3D12ResourceDatabase(IFenceSynchronizer fenceSynchronizer, D3D12DescriptorAllocator descriptorAllocator)
{ {
_fenceSynchronizer = fenceSynchronizer;
_descriptorAllocator = descriptorAllocator; _descriptorAllocator = descriptorAllocator;
_resources = new UnsafeSlotMap<ResourceRecord>(64, Allocator.Persistent, AllocationOption.Clear); _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); _meshes = new UnsafeSlotMap<Mesh>(64, Allocator.Persistent, AllocationOption.Clear);
_materials = new UnsafeSlotMap<Material>(16, Allocator.Persistent, AllocationOption.Clear); _materials = new UnsafeSlotMap<Material>(16, Allocator.Persistent, AllocationOption.Clear);
_shaders = new DynamicArray<Shader>(16); _shaders = new DynamicArray<Shader>(16);
_releaseQueue = new UnsafeQueue<ReleaseEntry>(32, Allocator.Persistent);
} }
~D3D12ResourceDatabase() ~D3D12ResourceDatabase()
@@ -124,14 +137,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase
Dispose(); Dispose();
} }
private void ReleaseResource<T>(ref T resource) private void ReleaseResource<T>(T resource)
where T : IResourceReleasable where T : IResourceReleasable
{ {
resource.ReleaseResource(this); 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); ObjectDisposedException.ThrowIf(_disposed, this);
@@ -157,7 +169,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return handle; 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); ObjectDisposedException.ThrowIf(_disposed, this);
if (allocation == null) if (allocation == null)
@@ -168,7 +180,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return Handle<GPUResource>.Invalid; 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); var handle = new Handle<GPUResource>(id, generation);
#if DEBUG || GHOST_EDITOR #if DEBUG || GHOST_EDITOR
@@ -281,16 +293,29 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return null; return null;
} }
// FIX: This should be queued to be released after GPU is done with it. public void ScheduleReleaseResource(Handle<GPUResource> handle)
public void ReleaseResource(Handle<GPUResource> handle)
{ {
ObjectDisposedException.ThrowIf(_disposed, this); ObjectDisposedException.ThrowIf(_disposed, this);
if (!handle.IsValid) if (_resources.TryGetElementAt(handle.ID, handle.Generation, out var record))
{ {
return; 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); ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist || !info.Allocated) if (!exist || !info.Allocated)
{ {
@@ -298,10 +323,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
} }
info.Release(_descriptorAllocator); info.Release(_descriptorAllocator);
#if DEBUG || GHOST_EDITOR
_resourceName.Remove(handle, out var name);
#endif
_resources.Remove(handle.ID, handle.Generation); _resources.Remove(handle.ID, handle.Generation);
} }
@@ -370,7 +391,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return; return;
} }
ReleaseResource(ref mesh); ReleaseResource(mesh);
_meshes.Remove(handle.ID, handle.Generation); _meshes.Remove(handle.ID, handle.Generation);
} }
@@ -409,7 +430,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return; return;
} }
ReleaseResource(ref material); ReleaseResource(material);
_materials.Remove(handle.ID, handle.Generation); _materials.Remove(handle.ID, handle.Generation);
} }
@@ -448,49 +469,64 @@ internal class D3D12ResourceDatabase : IResourceDatabase
} }
ref var shader = ref _shaders[id.Value]!; 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() 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) if (_disposed)
{ {
return; 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(); _resources.Dispose();
_samplers.Dispose(); _samplers.Dispose();
_meshes.Dispose(); _meshes.Dispose();
_materials.Dispose(); _materials.Dispose();
_releaseQueue.Dispose();
_disposed = true; _disposed = true;

View File

@@ -205,7 +205,7 @@ internal unsafe class D3D12SwapChain : ISwapChain
// Release old back buffers and render targets // Release old back buffers and render targets
for (var i = 0; i < _backBuffers.Count; i++) 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)); 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++) for (var i = 0; i < _backBuffers.Count; i++)
{ {
_resourceDatabase.ReleaseResource(_backBuffers[i].AsResource()); _resourceDatabase.ScheduleReleaseResource(_backBuffers[i].AsResource());
} }
_backBuffers.Dispose(); _backBuffers.Dispose();

View File

@@ -7,11 +7,11 @@ namespace Ghost.Graphics.D3D12.Utilities;
internal unsafe static class D3D12PipelineResource internal unsafe static class D3D12PipelineResource
{ {
private readonly static D3D12_INPUT_ELEMENT_DESC[] s_inputElementDescs = [ 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.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.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.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.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.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.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.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.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.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; public const DXGI_FORMAT SWAP_CHAIN_BACK_BUFFER_FORMAT = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;

View File

@@ -17,15 +17,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="runtime\win-x64\native\dxcompiler.dll" /> <Content Include="runtimes\win-x64\native\dxcompiler.dll">
<None Remove="runtime\win-x64\native\dxil.dll" />
</ItemGroup>
<ItemGroup>
<Content Include="runtime\win-x64\native\dxcompiler.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="runtime\win-x64\native\dxil.dll"> <Content Include="runtimes\win-x64\native\dxil.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ClassDiagram />

View File

@@ -96,10 +96,16 @@ public interface IResourceDatabase : IDisposable
string? GetResourceName(Handle<GPUResource> handle); string? GetResourceName(Handle<GPUResource> handle);
/// <summary> /// <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> /// </summary>
/// <param name="handle">The handle of the resource to be removed.</param> /// <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> /// <summary>
/// Retrieves an existing sampler identifier that matches the specified description, or creates a new one if none /// Retrieves an existing sampler identifier that matches the specified description, or creates a new one if none

View File

@@ -212,10 +212,10 @@ internal sealed class RenderGraphCompiler
continue; continue;
} }
_graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource); _graphicsEngine.ResourceDatabase.ScheduleReleaseResource(res.backingResource);
} }
_graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); _graphicsEngine.ResourceDatabase.ScheduleReleaseResource(_resourceHeap);
} }
if (_aliasingManager.Heap.size == 0) if (_aliasingManager.Heap.size == 0)
@@ -380,11 +380,11 @@ internal sealed class RenderGraphCompiler
{ {
if (!res.isImported) 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; _resourceHeap = Handle<GPUResource>.Invalid;
} }
} }

View File

@@ -321,7 +321,7 @@ internal class MeshRenderPass : IRenderPass
{ {
foreach (var texture in _textures) foreach (var texture in _textures)
{ {
resourceDatabase.ReleaseResource(texture.AsResource()); resourceDatabase.ScheduleReleaseResource(texture.AsResource());
} }
} }
} }

View File

@@ -265,7 +265,6 @@ internal class RenderSystem : IRenderSystem
// Sync the current frame resource to this new fence to keep state consistent // Sync the current frame resource to this new fence to keep state consistent
frameResource.FenceValue = flushFence; frameResource.FenceValue = flushFence;
foreach (var resource in _frameResources) foreach (var resource in _frameResources)
{ {
resource.CommandAllocator.Reset(); resource.CommandAllocator.Reset();

View File

@@ -19,9 +19,9 @@ shader "Hidden/Blit"
includes includes
{ {
"F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.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/Color.hlsl";
"F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Properties.hlsl"; "F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl";
} }
hlsl hlsl

View File

@@ -5,7 +5,6 @@ using Misaki.HighPerformance.LowLevel.Buffer;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace Ghost.Entities.Test; namespace Ghost.Entities.Test;
@@ -66,8 +65,6 @@ public class QueryBenchmark
public void QueryEntities() public void QueryEntities()
{ {
ref var query = ref _world.ComponentManager.GetEntityQueryReference(_queryIdentifier); ref var query = ref _world.ComponentManager.GetEntityQueryReference(_queryIdentifier);
var vecDT = Vector256.Create(_dt);
foreach (var chunkView in query.GetChunkIterator()) foreach (var chunkView in query.GetChunkIterator())
{ {
var positions = chunkView.GetComponentDataRW<Position>(); var positions = chunkView.GetComponentDataRW<Position>();

View File

@@ -2,7 +2,6 @@ using Ghost.Core;
using Ghost.Graphics.Test.Windows; using Ghost.Graphics.Test.Windows;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
@@ -32,7 +31,7 @@ public partial class UnitTestApp : Application
OperatingSystem.IsLinux() ? "linux" : OperatingSystem.IsLinux() ? "linux" :
OperatingSystem.IsMacOS() ? "osx" : "unknown"; OperatingSystem.IsMacOS() ? "osx" : "unknown";
var arch = Environment.Is64BitProcess ? "x64" : "x86"; 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)) if (Directory.Exists(nativeDllDir))
{ {
foreach (var dll in Directory.EnumerateFiles(nativeDllDir, "*.dll")) foreach (var dll in Directory.EnumerateFiles(nativeDllDir, "*.dll"))

View File

@@ -5,6 +5,7 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -13,6 +14,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" /> <ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View 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;
}

View File

@@ -1,2 +1,4 @@
// See https://aka.ms/new-console-template for more information using Ghost.Test.Core;
Console.WriteLine("Hello, World!"); using Ghost.MicroTest;
TestRunner.Run<NvttBindingTest>();

View File

@@ -15,17 +15,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="fmod.dll" /> <None Update="runtimes\win-x64\native\fmod.dll">
<None Remove="fmodstudio.dll" />
</ItemGroup>
<ItemGroup>
<Content Include="runtime\win-x64\native\fmod.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </None>
<Content Include="runtime\win-x64\native\fmodstudio.dll"> <None Update="runtimes\win-x64\native\fmodstudio.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -16,9 +16,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Include="runtime\win-x64\native\meshoptimizer.dll"> <None Update="runtimes\win-x64\native\meshoptimizer.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -16,13 +16,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="runtime\win-x64\native\nvtt.dll" /> <Using Include="Ghost.Nvtt.Native" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="runtime\win-x64\native\nvtt.dll"> <None Update="runtimes\win-x64\native\nvtt.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View 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 |

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public static unsafe partial class Api public static unsafe partial class Api
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum EdgeFixup public enum EdgeFixup
{ {

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
/// <summary>Defines the annotation found in a native declaration.</summary> /// <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)] [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Diagnostics; 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> /// <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)] [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = false, Inherited = true)]

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttAlphaMode public enum NvttAlphaMode
{ {

View File

@@ -0,0 +1,6 @@
namespace Ghost.Nvtt.Native
{
public partial struct NvttBatchList
{
}
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttBoolean public enum NvttBoolean
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public partial struct NvttCPUInputBuffer public partial struct NvttCPUInputBuffer
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttChannelOrder public enum NvttChannelOrder
{ {

View File

@@ -0,0 +1,6 @@
namespace Ghost.Nvtt.Native
{
public partial struct NvttCompressionOptions
{
}
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttContainer public enum NvttContainer
{ {

View File

@@ -0,0 +1,6 @@
namespace Ghost.Nvtt.Native
{
public partial struct NvttContext
{
}
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttCubeLayout public enum NvttCubeLayout
{ {

View File

@@ -0,0 +1,6 @@
namespace Ghost.Nvtt.Native
{
public partial struct NvttCubeSurface
{
}
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttEncodeFlags public enum NvttEncodeFlags
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public unsafe partial struct NvttEncodeSettings public unsafe partial struct NvttEncodeSettings
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttError public enum NvttError
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttFormat public enum NvttFormat
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public partial struct NvttGPUInputBuffer public partial struct NvttGPUInputBuffer
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttInputFormat public enum NvttInputFormat
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttMipmapFilter public enum NvttMipmapFilter
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttNormalTransform public enum NvttNormalTransform
{ {

View File

@@ -0,0 +1,6 @@
namespace Ghost.Nvtt.Native
{
public partial struct NvttOutputOptions
{
}
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttPixelType public enum NvttPixelType
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttQuality public enum NvttQuality
{ {

View File

@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public unsafe partial struct NvttRefImage public unsafe partial struct NvttRefImage
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttResizeFilter public enum NvttResizeFilter
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttRoundMode public enum NvttRoundMode
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttSeverity public enum NvttSeverity
{ {

View File

@@ -0,0 +1,6 @@
namespace Ghost.Nvtt.Native
{
public partial struct NvttSurface
{
}
}

View File

@@ -0,0 +1,6 @@
namespace Ghost.Nvtt.Native
{
public partial struct NvttSurfaceSet
{
}
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttTextureType public enum NvttTextureType
{ {

View File

@@ -0,0 +1,6 @@
namespace Ghost.Nvtt.Native
{
public partial struct NvttTimingContext
{
}
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttToneMapper public enum NvttToneMapper
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttValueType public enum NvttValueType
{ {

View File

@@ -1,4 +1,4 @@
namespace Ghost.Nvtt namespace Ghost.Nvtt.Native
{ {
public enum NvttWrapMode public enum NvttWrapMode
{ {

View File

@@ -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));
}
} }
} }

View File

@@ -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));
}
} }
} }

View File

@@ -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));
}
} }
} }

View File

@@ -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
/// (05). The pointer is owned by this cube surface do NOT dispose it.
/// </summary>
public NvttSurface* FacePtr(int face)
{
ThrowIfDisposed();
return Api.nvttCubeSurfaceFace(_ptr, face);
}
// -------------------------------------------------------------------------
// Fold / Unfold
// -------------------------------------------------------------------------
/// <summary>
/// Folds a cross-layout <see cref="NvttSurface"/> into this cube surface.
/// </summary>
public void Fold(NvttSurfaceHandle img, NvttCubeLayout layout)
{
ThrowIfDisposed();
Api.nvttCubeSurfaceFold(_ptr, img.Ptr, layout);
}
/// <summary>
/// Unfolds the cube surface into a flat cross-layout image.
/// Caller owns the returned surface.
/// </summary>
public NvttSurfaceHandle Unfold(NvttCubeLayout layout)
{
ThrowIfDisposed();
return new NvttSurfaceHandle(Api.nvttCubeSurfaceUnfold(_ptr, layout));
}
// -------------------------------------------------------------------------
// Query methods
// -------------------------------------------------------------------------
/// <summary>Returns the per-channel average for the given channel index.</summary>
public float Average(int channel)
{
ThrowIfDisposed();
return Api.nvttCubeSurfaceAverage(_ptr, channel);
}
/// <summary>Returns the min and max values of a channel across all faces.</summary>
public void Range(int channel, out float min, out float max)
{
ThrowIfDisposed();
float lo, hi;
Api.nvttCubeSurfaceRange(_ptr, channel, &lo, &hi);
min = lo;
max = hi;
}
// -------------------------------------------------------------------------
// Pixel operations
// -------------------------------------------------------------------------
/// <summary>Clamps a channel to [<paramref name="low"/>, <paramref name="high"/>].</summary>
public void Clamp(int channel, float low, float high)
{
ThrowIfDisposed();
Api.nvttCubeSurfaceClamp(_ptr, channel, low, high);
}
/// <summary>Applies gamma expansion (toLinear) to all faces.</summary>
public void ToLinear(float gamma)
{
ThrowIfDisposed();
Api.nvttCubeSurfaceToLinear(_ptr, gamma);
}
/// <summary>Applies gamma compression to all faces.</summary>
public void ToGamma(float gamma)
{
ThrowIfDisposed();
Api.nvttCubeSurfaceToGamma(_ptr, gamma);
}
// -------------------------------------------------------------------------
// Filtering (return new owned NvttCubeSurface)
// -------------------------------------------------------------------------
/// <summary>
/// Computes an irradiance-filtered cube map of the given <paramref name="size"/>.
/// Caller owns the result.
/// </summary>
public NvttCubeSurfaceHandle IrradianceFilter(int size, EdgeFixup fixup = EdgeFixup.NVTT_EdgeFixup_None)
{
ThrowIfDisposed();
return new NvttCubeSurfaceHandle(Api.nvttCubeSurfaceIrradianceFilter(_ptr, size, fixup));
}
/// <summary>
/// Computes a cosine-power (specular) filtered cube map.
/// Caller owns the result.
/// </summary>
public NvttCubeSurfaceHandle CosinePowerFilter(int size, float cosinePower,
EdgeFixup fixup = EdgeFixup.NVTT_EdgeFixup_None)
{
ThrowIfDisposed();
return new NvttCubeSurfaceHandle(
Api.nvttCubeSurfaceCosinePowerFilter(_ptr, size, cosinePower, fixup));
}
/// <summary>
/// Resamples the cube map to the given <paramref name="size"/> using fast bilinear resampling.
/// Caller owns the result.
/// </summary>
public NvttCubeSurfaceHandle FastResample(int size, EdgeFixup fixup = EdgeFixup.NVTT_EdgeFixup_None)
{
ThrowIfDisposed();
return new NvttCubeSurfaceHandle(Api.nvttCubeSurfaceFastResample(_ptr, size, fixup));
}
// -------------------------------------------------------------------------
private void ThrowIfDisposed()
{
if (_ptr == null)
{
throw new ObjectDisposedException(nameof(NvttCubeSurfaceHandle));
}
} }
} }

View File

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

View File

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

View File

@@ -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));
}
} }
} }

View File

@@ -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));
}
} }
} }

View File

@@ -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));
}
} }
} }

View File

@@ -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));
}
} }
} }