diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/Asset.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/Asset.cs index 1ff9b50..d180522 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/Asset.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/Asset.cs @@ -1,8 +1,6 @@ -using Ghost.Core; using Ghost.Editor.Core.Contracts; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Serialization; namespace Ghost.Editor.Core.AssetHandler; @@ -178,8 +176,4 @@ public readonly struct AssetReference : IEquatable } } -public interface IAssetSettings -{ - ValueTask> WriteToStreamAsync(Stream stream, CancellationToken token = default); - ValueTask> ReadFromStreamAsync(Stream stream, CancellationToken token = default); -} +public interface IAssetSettings; diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs index 58a86f3..60dff94 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs @@ -11,29 +11,23 @@ public sealed class CustomAssetHandlerAttribute : Attribute get; init; } - public bool AllowCaching - { - get; init; - } = true; - public required string[] SupportedExtensions { get; init; } -} -public enum DependencyUpdateType -{ - Add, - Remove + public bool AllowCaching + { + get; init; + } = true; } public interface IAssetExportOptions; public interface IAssetHandler { - ValueTask> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default); - ValueTask SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default); + ValueTask> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default); + ValueTask SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default); } public interface IImportableAssetHandler : IAssetHandler diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs new file mode 100644 index 0000000..cee776e --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs @@ -0,0 +1,37 @@ +using Ghost.Editor.Core.Contracts; + +namespace Ghost.Editor.Core.AssetHandler; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class CustomAssetProcesserAttribute : 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); +} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs index 509b122..b660b18 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs @@ -41,13 +41,6 @@ public enum TextureCompressionLevel : uint High } -public enum TextureCompressionEffort : uint -{ - Fastest, - Normal, - Production -} - public enum MipmapFilter : uint { Box, @@ -59,14 +52,17 @@ public enum MipmapFilter : uint public class TextureAsset : Asset { internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F"; - internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID); - public override Guid TypeID => s_typeGuid; + private readonly Handle _texture; - public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings) + public override Guid TypeID => s_typeGuid; + public Handle Texture => _texture; + + public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings, Handle texture) : base(id, dependencies, settings) { + _texture = texture; } } @@ -142,21 +138,16 @@ public class TextureAssetSettings : IAssetSettings get; set; } = TextureCompressionLevel.Normal; - public TextureCompressionEffort CompressionEffort - { - get; set; - } = TextureCompressionEffort.Normal; - public bool UseBorderColor { get; set; } = false; - public Color32 BorderColor + public Color128 BorderColor { get; set; - } = new Color32(0, 0, 0, 0); - + } = new Color128(0, 0, 0, 0); + public bool ZeroAlphaBorder { get; set; @@ -254,11 +245,12 @@ public class TextureAssetSettings : IAssetSettings try { - ref byte address = ref MemoryMarshal.GetReference(tempArray); await stream.ReadAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false); - var basic = Unsafe.ReadUnaligned(ref address); - var advanced = Unsafe.ReadUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf())); - var sampler = Unsafe.ReadUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf() + Unsafe.SizeOf())); + + // Use index-based reads after the await to avoid 'ref across await' errors. + var basic = Unsafe.ReadUnaligned(ref tempArray[0]); + var advanced = Unsafe.ReadUnaligned(ref tempArray[Unsafe.SizeOf()]); + var sampler = Unsafe.ReadUnaligned(ref tempArray[Unsafe.SizeOf() + Unsafe.SizeOf()]); var settings = new TextureAssetSettings { @@ -280,6 +272,7 @@ public class TextureAssetSettings : IAssetSettings } } +[CustomAssetHandler(ID = TextureAsset._TYPE_ID, SupportedExtensions = new[] { ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" })] internal class TextureAssetHandler : IImportableAssetHandler { private const int _CURRENT_VERSION = 1; @@ -291,87 +284,112 @@ internal class TextureAssetHandler : IImportableAssetHandler public async ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default) { + // ---- 1. Probe image info ----------------------------------------------- var info = ImageInfo.FromStream(sourceStream); if (info.BitsPerChannel <= 0) - { return Result.Failure($"Unsupported image format with {info.BitsPerChannel} bits per channel."); - } - ref byte pData = ref Unsafe.NullRef(); - var imageSize = 0ul; var isFloat = info.BitsPerChannel > 8; + var width = info.Width; + var height = info.Height; + var colorComponents = info.ColorComponents; + + // ---- 2. Decode pixels into a managed byte[] ---------------------------- + byte[] pixelBytes; if (isFloat) { - using var image = ImageResultFloat.FromStream(sourceStream, info.ColorComponents); - pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan())); - imageSize = image.Size; + using var image = ImageResultFloat.FromStream(sourceStream, colorComponents); + var span = MemoryMarshal.AsBytes(image.AsSpan()); + pixelBytes = new byte[span.Length]; + span.CopyTo(pixelBytes); } else { - using var image = ImageResult.FromStream(sourceStream, info.ColorComponents); - pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan())); - imageSize = image.Size; + using var image = ImageResult.FromStream(sourceStream, colorComponents); + var span = MemoryMarshal.AsBytes(image.AsSpan()); + pixelBytes = new byte[span.Length]; + span.CopyTo(pixelBytes); } + // ---- 3. Run NVTT compression on a thread-pool thread (side-effect only) - + // The cache path is derivable at any time from (id, settingsHash), so we + // do NOT store it in the asset file. LoadAsync/SaveAsync will recompute it. + var settings = new TextureAssetSettings(); + await Task.Run(() => + TextureProcessor.CompressToCache( + EditorApplication.CachesFolderPath, + id, + pixelBytes, + width, + height, + isFloat, + colorComponents, + settings), + token).ConfigureAwait(false); + + // ---- 4. Write asset file: header + settings + raw image data ----------- + // Content layout (all little-endian): + // int32 width + // int32 height + // byte isFloat (0 = byte, 1 = float) + // int32 colorComponents (cast of ColorComponents enum) + // byte[] pixelBytes + const int _CONTENT_HEADER_SIZE = 4 + 4 + 1 + 4; // 13 bytes + var contentSize = _CONTENT_HEADER_SIZE + pixelBytes.Length; + var header = new AssetMetadata(id, TextureAsset.s_typeGuid) { HandlerVersion = _CURRENT_VERSION, SettingsOffset = AssetMetadata.SIZE, }; + // Reserve space for the header, then write settings targetStream.Seek(0, SeekOrigin.Begin); AssetMetadata.WriteToStream(targetStream, ref header); targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin); - var settings = new TextureAssetSettings(); var sizeResult = await settings.WriteToStreamAsync(targetStream, token).ConfigureAwait(false); if (sizeResult.IsFailure) - { return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}"); - } - header.SettingsSize = sizeResult.Value; + header.SettingsSize = sizeResult.Value; header.ContentOffset = header.SettingsOffset + sizeResult.Value; - header.ContentSize = (long)imageSize; + header.ContentSize = contentSize; + // Write raw image content targetStream.Seek(header.ContentOffset, SeekOrigin.Begin); - var offset = 0; - var tempArray = ArrayPool.Shared.Rent((int)Math.Min(imageSize, 40960ul)); - var remaining = imageSize; - + var contentHeader = ArrayPool.Shared.Rent(_CONTENT_HEADER_SIZE); try { - while (remaining > 0) - { - var chunkSize = (int)Math.Min(remaining, (ulong)tempArray.Length); - Unsafe.CopyBlockUnaligned(ref tempArray[0], ref Unsafe.Add(ref pData, offset), (uint)chunkSize); - - await targetStream.WriteAsync(tempArray.AsMemory(0, chunkSize), token).ConfigureAwait(false); - - offset += chunkSize; - remaining -= (ulong)chunkSize; - } - - return Result.Success(); - } - catch (Exception ex) - { - return Result.Failure($"Failed to write texture asset content to stream: {ex.Message}"); + BitConverter.TryWriteBytes(contentHeader.AsSpan(0, 4), width); + BitConverter.TryWriteBytes(contentHeader.AsSpan(4, 4), height); + contentHeader[8] = isFloat ? (byte)1 : (byte)0; + BitConverter.TryWriteBytes(contentHeader.AsSpan(9, 4), (int)colorComponents); + await targetStream.WriteAsync(contentHeader.AsMemory(0, _CONTENT_HEADER_SIZE), token).ConfigureAwait(false); } finally { - ArrayPool.Shared.Return(tempArray); + ArrayPool.Shared.Return(contentHeader); } + + await targetStream.WriteAsync(pixelBytes, token).ConfigureAwait(false); + await targetStream.FlushAsync(token).ConfigureAwait(false); + + // Patch header now that all sizes are known + targetStream.Seek(0, SeekOrigin.Begin); + AssetMetadata.WriteToStream(targetStream, ref header); + + return Result.Success(); } - public ValueTask> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default) + public ValueTask> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default) { throw new NotImplementedException(); } - public ValueTask SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default) + public ValueTask SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default) { throw new NotImplementedException(); } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs new file mode 100644 index 0000000..3a7ebcf --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs @@ -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; + +/// +/// 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 (CachesFolderPath/TextureCache/<guid>_<hash>.dds). +/// 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. +/// +internal static unsafe class TextureProcessor +{ + private const string _TEXTURE_CACHE_SUBFOLDER = "TextureCache"; + + // ------------------------------------------------------------------------- + // Public entry point + // ------------------------------------------------------------------------- + + /// + /// Compresses according to + /// 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. + /// + public static string CompressToCache( + string cachesFolderPath, + Guid assetId, + ReadOnlySpan 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 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, + }; + + /// + /// Produces a stable 64-bit hash of the settings structs so the cache file + /// name changes whenever any setting changes. + /// + private static ulong ComputeSettingsHash(TextureAssetSettings s) + { + var basicSize = Unsafe.SizeOf(); + var advancedSize = Unsafe.SizeOf(); + var samplerSize = Unsafe.SizeOf(); + var total = basicSize + advancedSize + samplerSize; + + Span 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); + } +} diff --git a/src/Editor/Ghost.Editor.Core/EditorApplication.cs b/src/Editor/Ghost.Editor.Core/EditorApplication.cs index 67cad2a..2b72005 100644 --- a/src/Editor/Ghost.Editor.Core/EditorApplication.cs +++ b/src/Editor/Ghost.Editor.Core/EditorApplication.cs @@ -1,4 +1,3 @@ -using Ghost.Editor.Core.Contracts; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; @@ -7,7 +6,10 @@ namespace Ghost.Editor.Core; public static class EditorApplication { public const string ASSETS_FOLDER_NAME = "Assets"; + public const string SOURCES_FOLDER_NAME = "Sources"; + public const string PACKAGES_FOLDER_NAME = "Packages"; public const string CACHES_FOLDER_NAME = "Caches"; + public const string CONFIG_FOLDER_NAME = "Config"; private static IServiceProvider? s_serviceProvider; private static string s_currentProjectPath = string.Empty; @@ -16,8 +18,15 @@ public static class EditorApplication private static DispatcherQueue? s_dispatcherQueue; internal static Application CurrentApplication => Application.Current; - internal static string CurrentProjectPath => s_currentProjectPath; - internal static string CurrentProjectName => s_currentProjectName; + + public static string ProjectPath => s_currentProjectPath; + public static string ProjectName => s_currentProjectName; + + public static string AssetsFolderPath => Path.Combine(ProjectPath, ASSETS_FOLDER_NAME); + public static string SourcesFolderPath => Path.Combine(ProjectPath, SOURCES_FOLDER_NAME); + public static string PackagesFolderPath => Path.Combine(ProjectPath, PACKAGES_FOLDER_NAME); + public static string CachesFolderPath => Path.Combine(ProjectPath, CACHES_FOLDER_NAME); + public static string ConfigFolderPath => Path.Combine(ProjectPath, CONFIG_FOLDER_NAME); public static DispatcherQueue DispatcherQueue { @@ -57,9 +66,5 @@ public static class EditorApplication internal static void Shutdown() { - if (s_serviceProvider?.GetService(typeof(IAssetService)) is AssetHandle.AssetService assetService) - { - assetService.Shutdown(); - } } } \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj index 4039b49..9112519 100644 --- a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj +++ b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs index c7acb92..c02ff94 100644 --- a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs @@ -39,7 +39,7 @@ internal sealed partial class EngineEditorWindow : WindowEx private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { - PART_TitleBar.Title = EditorApplication.CurrentProjectName; + PART_TitleBar.Title = EditorApplication.ProjectName; PART_TitleBar.Subtitle = $"Ghost Engine {Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}"; _notificationService.SetReference(InfoBar, NotificationQueue); diff --git a/src/Editor/Ghost.Editor/View/Windows/SplashWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/SplashWindow.xaml.cs index 77204be..a361c0a 100644 --- a/src/Editor/Ghost.Editor/View/Windows/SplashWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/SplashWindow.xaml.cs @@ -26,7 +26,7 @@ internal sealed partial class SplashWindow : WindowEx { var version = Package.Current.Id.Version; VersionTextBlock.Text = $"Version {version.Major}.{version.Minor}.{version.Build}"; - LoadingTextBlock.Text = $"Loading {EditorApplication.CurrentProjectName}..."; + LoadingTextBlock.Text = $"Loading {EditorApplication.ProjectName}..."; CopyrightTextBlock.Text = $"Copyright © {DateTime.Now.Year} Ghost Engine. All rights reserved."; } } diff --git a/src/Editor/Ghost.Editor/ViewModels/Controls/ProjectBrowserViewModel.cs b/src/Editor/Ghost.Editor/ViewModels/Controls/ProjectBrowserViewModel.cs index b0d6a81..cb61fb2 100644 --- a/src/Editor/Ghost.Editor/ViewModels/Controls/ProjectBrowserViewModel.cs +++ b/src/Editor/Ghost.Editor/ViewModels/Controls/ProjectBrowserViewModel.cs @@ -46,7 +46,7 @@ internal partial class ProjectBrowserViewModel : ObservableObject _inspectorService = inspectorService; _assetService = assetService; - var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); + var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); LoadSubFolderRecursive(assetsRootItem); Directories.Add(assetsRootItem); diff --git a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs b/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs index 6a199e8..1b612cd 100644 --- a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs +++ b/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs @@ -41,7 +41,7 @@ internal partial class ProjectViewModel : ObservableObject { _assetService = assetService; - var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); + var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); LoadSubFolderRecursive(ref assetsRootItem); SubDirectories.Add(assetsRootItem); diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index 41339dd..a99677c 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -21,10 +21,13 @@ - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/src/Runtime/Ghost.Engine/EngineCore.cs b/src/Runtime/Ghost.Engine/EngineCore.cs index 703b7b5..28ca6be 100644 --- a/src/Runtime/Ghost.Engine/EngineCore.cs +++ b/src/Runtime/Ghost.Engine/EngineCore.cs @@ -1,11 +1,13 @@ using Ghost.Entities; +using Ghost.Graphics; using Misaki.HighPerformance.Jobs; namespace Ghost.Engine; public interface IEngineContext : IDisposable { - JobScheduler JobScheduler { get; } + IJobScheduler JobScheduler { get; } + IRenderSystem RenderSystem { get; } } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] @@ -17,13 +19,24 @@ internal class EngineEntryAttribute : Attribute internal sealed partial class EngineCore : IEngineContext { private readonly JobScheduler _jobScheduler; + private readonly RenderSystem _renderSystem; - public JobScheduler JobScheduler => _jobScheduler; + public IJobScheduler JobScheduler => _jobScheduler; + public IRenderSystem RenderSystem => _renderSystem; public EngineCore() { _jobScheduler = new JobScheduler(Environment.ProcessorCount - 2); // We -2 here, one for main thread, one for render thread + // TODO: Remove the windows dependency from RenderSystem. + var renderingConfig = new RenderingConfig + { + FrameBufferCount = 2, + GraphicsAPI = GraphicsAPI.Direct3D12, + }; + + _renderSystem = new RenderSystem(renderingConfig); + ComponentRegistry.GetOrRegisterComponentID(); } diff --git a/src/Runtime/Ghost.Entities/EntityQuery.JobChunk.cs b/src/Runtime/Ghost.Entities/EntityQuery.JobChunk.cs index b1a2743..baac511 100644 --- a/src/Runtime/Ghost.Entities/EntityQuery.JobChunk.cs +++ b/src/Runtime/Ghost.Entities/EntityQuery.JobChunk.cs @@ -82,7 +82,7 @@ public unsafe partial struct EntityQuery chunkInfos = chunkInfos.AsReadOnly() }; - var handle = world.JobScheduler.ScheduleParallel(ref batchJob, chunkInfos.Count, batchSize, dependency); + var handle = world.JobScheduler.ScheduleParallelFor(ref batchJob, chunkInfos.Count, batchSize, dependency); var disposeJob = new DisposeJobChunk { diff --git a/src/Runtime/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs b/src/Runtime/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs index 6f28797..cb8f21e 100644 --- a/src/Runtime/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs +++ b/src/Runtime/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs @@ -1178,7 +1178,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity1 @@ -1334,7 +1334,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity2 @@ -1517,7 +1517,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity3 @@ -1727,7 +1727,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity4 @@ -1964,7 +1964,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity5 @@ -2228,7 +2228,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity6 @@ -2519,7 +2519,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity7 @@ -2837,7 +2837,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity8 diff --git a/src/Runtime/Ghost.Entities/Templates/EntityQuery.JobEntity.tt b/src/Runtime/Ghost.Entities/Templates/EntityQuery.JobEntity.tt index 26eeff8..1b21ccd 100644 --- a/src/Runtime/Ghost.Entities/Templates/EntityQuery.JobEntity.tt +++ b/src/Runtime/Ghost.Entities/Templates/EntityQuery.JobEntity.tt @@ -216,7 +216,7 @@ public unsafe partial struct EntityQuery } } - var jobHandle = world.JobScheduler.ScheduleParallel(ref runner, chunks.Count, batchSize, dependency); + var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); // 3. Dispose the temp lists var disposeJob = new DisposeJobEntity<#= i #> diff --git a/src/Runtime/Ghost.Graphics/Core/Material.cs b/src/Runtime/Ghost.Graphics/Core/Material.cs index 6cc9340..095c891 100644 --- a/src/Runtime/Ghost.Graphics/Core/Material.cs +++ b/src/Runtime/Ghost.Graphics/Core/Material.cs @@ -35,10 +35,9 @@ internal struct CBufferCache : IResourceReleasable } _cpuData.Dispose(); + database.ScheduleReleaseResource(_gpuResource.AsResource()); - database.ReleaseResource(GpuResource.AsResource()); _gpuResource = Handle.Invalid; - _size = 0; } } diff --git a/src/Runtime/Ghost.Graphics/Core/Mesh.cs b/src/Runtime/Ghost.Graphics/Core/Mesh.cs index 774eb0e..d8f560d 100644 --- a/src/Runtime/Ghost.Graphics/Core/Mesh.cs +++ b/src/Runtime/Ghost.Graphics/Core/Mesh.cs @@ -117,9 +117,9 @@ public struct Mesh : IResourceReleasable { ReleaseCpuResources(); - database.ReleaseResource(VertexBuffer.AsResource()); - database.ReleaseResource(IndexBuffer.AsResource()); - database.ReleaseResource(ObjectDataBuffer.AsResource()); + database.ScheduleReleaseResource(VertexBuffer.AsResource()); + database.ScheduleReleaseResource(IndexBuffer.AsResource()); + database.ScheduleReleaseResource(ObjectDataBuffer.AsResource()); } } diff --git a/src/Runtime/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs b/src/Runtime/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs index edb40e2..5601719 100644 --- a/src/Runtime/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs +++ b/src/Runtime/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs @@ -47,9 +47,9 @@ internal class D3D12GraphicsEngine : IGraphicsEngine _shaderCompiler = new DxcShaderCompiler(); _descriptorAllocator = new D3D12DescriptorAllocator(_device); - _resourceDatabase = new D3D12ResourceDatabase(_descriptorAllocator); + _resourceDatabase = new D3D12ResourceDatabase(renderSystem, _descriptorAllocator); _pipelineLibrary = new D3D12PipelineLibrary(_device, _resourceDatabase); - _resourceAllocator = new D3D12ResourceAllocator(renderSystem, _device, _descriptorAllocator, _resourceDatabase, _pipelineLibrary); + _resourceAllocator = new D3D12ResourceAllocator(_device, _descriptorAllocator, _resourceDatabase, _pipelineLibrary); _renderers = ImmutableArray.Empty; @@ -127,7 +127,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine } } - _resourceAllocator.ReleaseTempResources(); + _resourceDatabase.EndFrame(); return r; } @@ -144,6 +144,8 @@ internal class D3D12GraphicsEngine : IGraphicsEngine renderer.Dispose(); } + _resourceDatabase.ReleaseAllResourcesImmediately(); + _resourceAllocator.Dispose(); _pipelineLibrary.Dispose(); _resourceDatabase.Dispose(); diff --git a/src/Runtime/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs b/src/Runtime/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs index 05445b4..32bbff4 100644 --- a/src/Runtime/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs +++ b/src/Runtime/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs @@ -5,11 +5,8 @@ using Ghost.Graphics.Core; using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel; -using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; -using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -454,21 +451,18 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator private UniquePtr _d3d12MA; - private readonly IFenceSynchronizer _fenceSynchronizer; private readonly D3D12RenderDevice _device; private readonly D3D12DescriptorAllocator _descriptorAllocator; private readonly D3D12ResourceDatabase _resourceDatabase; private readonly D3D12PipelineLibrary _pipelineLibrary; - private UnsafeQueue> _tempResources; - + // TODO: We should use ring buffer pool in d3d12ma for upload buffer. private readonly Handle _uploadBatch; private ulong _uploadBatchOffset; private bool _disposed; public D3D12ResourceAllocator( - IFenceSynchronizer fenceSynchronizer, D3D12RenderDevice device, D3D12DescriptorAllocator descriptorAllocator, D3D12ResourceDatabase resourceDatabase, @@ -485,14 +479,11 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator ThrowIfFailed(D3D12MA_CreateAllocator(&desc, &pAllocator)); _d3d12MA.Attach(pAllocator); - _fenceSynchronizer = fenceSynchronizer; _device = device; _descriptorAllocator = descriptorAllocator; _resourceDatabase = resourceDatabase; _pipelineLibrary = pipelineLibrary; - _tempResources = new UnsafeQueue>(64, Allocator.Persistent); - // Create an upload batch var uploadDesc = new BufferDesc { @@ -513,13 +504,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator [MethodImpl(MethodImplOptions.AggressiveInlining)] private Handle TrackAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData barrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string name, bool isTemp) { - var handle = _resourceDatabase.AddAllocation(allocation, _fenceSynchronizer.CPUFenceValue, barrierData, resourceDescriptor, desc, name); - - if (isTemp) - { - _tempResources.Enqueue(handle); - } - + var handle = _resourceDatabase.AddAllocation(allocation, barrierData, resourceDescriptor, desc, name); return handle; } @@ -844,7 +829,10 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator }; offset = 0; - return CreateBuffer(in bufferDesc, "TempUploadBuffer", options); + var handle = CreateBuffer(in bufferDesc, "TempUploadBuffer", options); + + _resourceDatabase.ScheduleReleaseResource(handle.AsResource()); + return handle; } } @@ -943,35 +931,6 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator return _resourceDatabase.AddShader(shader); } - public void ReleaseTempResources() - { - ObjectDisposedException.ThrowIf(_disposed, this); - - while (_tempResources.Count > 0) - { - var handle = _tempResources.Peek(); - var r = _resourceDatabase.GetResourceRecord(handle); - if (r.IsFailure || !r.Value.Allocated) - { - // Resource already released or invalid, just dequeue - _tempResources.Dequeue(); - continue; - } - - if (r.Value.cpuFenceValue > _fenceSynchronizer.GPUFenceValue) - { - // Resource still in use by GPU, stop processing. - // Since resources are enqueued in order, we can break here. - break; - } - - _resourceDatabase.ReleaseResource(handle); - _tempResources.Dequeue(); - } - - _uploadBatchOffset = 0; - } - public void Dispose() { if (_disposed) @@ -979,17 +938,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator return; } - Debug.Assert(_tempResources.Count == 0, "Temporary resources should be released before disposing the allocator."); - - foreach (var handle in _tempResources) - { - _resourceDatabase.ReleaseResource(handle); - } - - _resourceDatabase.ReleaseResource(_uploadBatch.AsResource()); - + _resourceDatabase.ReleaseResourceImmediately(_uploadBatch.AsResource()); _d3d12MA.Dispose(); - _tempResources.Dispose(); _disposed = true; GC.SuppressFinalize(this); diff --git a/src/Runtime/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs b/src/Runtime/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs index 0b8f3b2..987409d 100644 --- a/src/Runtime/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs +++ b/src/Runtime/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs @@ -6,8 +6,6 @@ using Misaki.HighPerformance.Collections; using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using TerraFX.Interop.DirectX; @@ -43,19 +41,17 @@ internal class D3D12ResourceDatabase : IResourceDatabase public ResourceBarrierData barrierData; - public uint cpuFenceValue; public readonly bool isExternal; public readonly bool Allocated => isExternal ? resource.resource.Get() != null : resource.allocation.Get() != null; public readonly SharedPtr ResourcePtr => isExternal ? resource.resource.Get() : resource.allocation.Get()->GetResource(); - public ResourceRecord(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceBarrierData barrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc) + public ResourceRecord(D3D12MA_Allocation* allocation, ResourceBarrierData barrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc) { this.resource = new ResourceUnion(allocation); this.isExternal = false; this.viewGroup = resourceDescriptor; - this.cpuFenceValue = cpuFenceValue; this.barrierData = barrierData; this.desc = desc; } @@ -66,7 +62,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase this.isExternal = true; this.viewGroup = viewGroup; - this.cpuFenceValue = ~0u; this.barrierData = barrierData; this.desc = resource->GetDesc().ToResourceDesc(); } @@ -91,6 +86,19 @@ internal class D3D12ResourceDatabase : IResourceDatabase } } + private readonly struct ReleaseEntry + { + public readonly ResourceRecord record; + public readonly uint fenceValue; + + public ReleaseEntry(ResourceRecord record, uint fenceValue) + { + this.record = record; + this.fenceValue = fenceValue; + } + } + + private readonly IFenceSynchronizer _fenceSynchronizer; private readonly D3D12DescriptorAllocator _descriptorAllocator; private UnsafeSlotMap _resources; @@ -103,10 +111,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase private UnsafeSlotMap _materials; private readonly DynamicArray _shaders; // TODO: Use SlotMap? + private UnsafeQueue _releaseQueue; + private bool _disposed; - public D3D12ResourceDatabase(D3D12DescriptorAllocator descriptorAllocator) + public D3D12ResourceDatabase(IFenceSynchronizer fenceSynchronizer, D3D12DescriptorAllocator descriptorAllocator) { + _fenceSynchronizer = fenceSynchronizer; _descriptorAllocator = descriptorAllocator; _resources = new UnsafeSlotMap(64, Allocator.Persistent, AllocationOption.Clear); @@ -117,6 +128,8 @@ internal class D3D12ResourceDatabase : IResourceDatabase _meshes = new UnsafeSlotMap(64, Allocator.Persistent, AllocationOption.Clear); _materials = new UnsafeSlotMap(16, Allocator.Persistent, AllocationOption.Clear); _shaders = new DynamicArray(16); + + _releaseQueue = new UnsafeQueue(32, Allocator.Persistent); } ~D3D12ResourceDatabase() @@ -124,14 +137,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase Dispose(); } - private void ReleaseResource(ref T resource) + private void ReleaseResource(T resource) where T : IResourceReleasable { resource.ReleaseResource(this); - resource = default!; } - public unsafe Handle ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, string? name = null) + internal unsafe Handle ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, string? name = null) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -157,7 +169,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase return handle; } - public unsafe Handle AddAllocation(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) + public unsafe Handle AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) { ObjectDisposedException.ThrowIf(_disposed, this); if (allocation == null) @@ -168,7 +180,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase return Handle.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(id, generation); #if DEBUG || GHOST_EDITOR @@ -281,16 +293,29 @@ internal class D3D12ResourceDatabase : IResourceDatabase return null; } - // FIX: This should be queued to be released after GPU is done with it. - public void ReleaseResource(Handle handle) + public void ScheduleReleaseResource(Handle handle) { ObjectDisposedException.ThrowIf(_disposed, this); - if (!handle.IsValid) + if (_resources.TryGetElementAt(handle.ID, handle.Generation, out var record)) { return; } + var entry = new ReleaseEntry(record, _fenceSynchronizer.CPUFenceValue); + + _releaseQueue.Enqueue(entry); + _resources.Remove(handle.ID, handle.Generation); + +#if DEBUG || GHOST_EDITOR + _resourceName.Remove(handle, out var name); +#endif + } + + public void ReleaseResourceImmediately(Handle handle) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist || !info.Allocated) { @@ -298,10 +323,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase } info.Release(_descriptorAllocator); -#if DEBUG || GHOST_EDITOR - _resourceName.Remove(handle, out var name); -#endif - _resources.Remove(handle.ID, handle.Generation); } @@ -370,7 +391,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase return; } - ReleaseResource(ref mesh); + ReleaseResource(mesh); _meshes.Remove(handle.ID, handle.Generation); } @@ -409,7 +430,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase return; } - ReleaseResource(ref material); + ReleaseResource(material); _materials.Remove(handle.ID, handle.Generation); } @@ -448,49 +469,64 @@ internal class D3D12ResourceDatabase : IResourceDatabase } ref var shader = ref _shaders[id.Value]!; - ReleaseResource(ref shader); + ReleaseResource(shader); + } + + public void EndFrame() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + while (_releaseQueue.Count > 0) + { + var toRelease = _releaseQueue.Peek(); + if (toRelease.fenceValue > _fenceSynchronizer.GPUFenceValue) + { + break; + } + + _releaseQueue.Dequeue(); + + toRelease.record.Release(_descriptorAllocator); + } + } + + internal void ReleaseAllResourcesImmediately() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + foreach (var mesh in _meshes) + { + ReleaseResource(mesh); + } + + foreach (var material in _materials) + { + ReleaseResource(material); + } + + foreach (var shader in _shaders) + { + ReleaseResource(shader); + } + + foreach (ref var record in _resources) + { + record.Release(_descriptorAllocator); + } } public void Dispose() { - [DoesNotReturn] - [Conditional("DEBUG")] - static void ThrowMemoryLeakException(string resourceType, int count) - { - throw new MemoryLeakException($"ResourceAllocator is being disposed with {count} {resourceType} still registered. Ensure all resources are released before disposing."); - } - if (_disposed) { return; } - if (_resources.Count > 0) - { - ThrowMemoryLeakException("GPU resources", _resources.Count); - } - - if (_meshes.Count > 0) - { - ThrowMemoryLeakException("meshes", _meshes.Count); - } - - if (_materials.Count > 0) - { - ThrowMemoryLeakException("materials", _materials.Count); - } - - // DSL are reference space, it will be managed by GC, so we don't throw exception here. - for (var i = 0; i < _shaders.Count; i++) - { - ref var shader = ref _shaders[i]; - ReleaseResource(ref shader); - } - _resources.Dispose(); _samplers.Dispose(); _meshes.Dispose(); _materials.Dispose(); + _releaseQueue.Dispose(); _disposed = true; diff --git a/src/Runtime/Ghost.Graphics/D3D12/D3D12SwapChain.cs b/src/Runtime/Ghost.Graphics/D3D12/D3D12SwapChain.cs index 2ba96db..05b5d8f 100644 --- a/src/Runtime/Ghost.Graphics/D3D12/D3D12SwapChain.cs +++ b/src/Runtime/Ghost.Graphics/D3D12/D3D12SwapChain.cs @@ -205,7 +205,7 @@ internal unsafe class D3D12SwapChain : ISwapChain // Release old back buffers and render targets for (var i = 0; i < _backBuffers.Count; i++) { - _resourceDatabase.ReleaseResource(_backBuffers[i].AsResource()); + _resourceDatabase.ReleaseResourceImmediately(_backBuffers[i].AsResource()); } ThrowIfFailed(_swapChain.Get()->ResizeBuffers((uint)_backBuffers.Count, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, (uint)DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING)); @@ -252,7 +252,7 @@ internal unsafe class D3D12SwapChain : ISwapChain for (var i = 0; i < _backBuffers.Count; i++) { - _resourceDatabase.ReleaseResource(_backBuffers[i].AsResource()); + _resourceDatabase.ScheduleReleaseResource(_backBuffers[i].AsResource()); } _backBuffers.Dispose(); diff --git a/src/Runtime/Ghost.Graphics/D3D12/Utilities/D3D12PipelineResource.cs b/src/Runtime/Ghost.Graphics/D3D12/Utilities/D3D12PipelineResource.cs index af52971..149eae1 100644 --- a/src/Runtime/Ghost.Graphics/D3D12/Utilities/D3D12PipelineResource.cs +++ b/src/Runtime/Ghost.Graphics/D3D12/Utilities/D3D12PipelineResource.cs @@ -7,11 +7,11 @@ namespace Ghost.Graphics.D3D12.Utilities; internal unsafe static class D3D12PipelineResource { private readonly static D3D12_INPUT_ELEMENT_DESC[] s_inputElementDescs = [ - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Position.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 0u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Normal.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 16u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Tangent.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 32u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Uv.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 48u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Color.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 64u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Position.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 0u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Normal.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 16u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Tangent.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 32u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Uv.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 48u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Color.GetUnsafePtr(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 64u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, ]; public const DXGI_FORMAT SWAP_CHAIN_BACK_BUFFER_FORMAT = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; diff --git a/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj b/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj index 781f02f..b492afb 100644 --- a/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj +++ b/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj @@ -17,15 +17,10 @@ - - - - - - + PreserveNewest - + PreserveNewest diff --git a/src/Runtime/Ghost.Graphics/RHI/ClassDiagram1.cd b/src/Runtime/Ghost.Graphics/RHI/ClassDiagram1.cd deleted file mode 100644 index 7b89419..0000000 --- a/src/Runtime/Ghost.Graphics/RHI/ClassDiagram1.cd +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/src/Runtime/Ghost.Graphics/RHI/IResourceDatabase.cs b/src/Runtime/Ghost.Graphics/RHI/IResourceDatabase.cs index caa03b6..36e1b96 100644 --- a/src/Runtime/Ghost.Graphics/RHI/IResourceDatabase.cs +++ b/src/Runtime/Ghost.Graphics/RHI/IResourceDatabase.cs @@ -96,10 +96,16 @@ public interface IResourceDatabase : IDisposable string? GetResourceName(Handle handle); /// - /// Removes a resource from the database using its handle. + /// Releases the GPU resource associated with the specified handle, freeing any resources allocated to it. /// /// The handle of the resource to be removed. - void ReleaseResource(Handle handle); + void ScheduleReleaseResource(Handle handle); + + /// + /// Releases the GPU resource associated with the specified handle immediately, freeing any resources allocated to it. + /// + /// The handle of the resource to be removed. + void ReleaseResourceImmediately(Handle handle); /// /// Retrieves an existing sampler identifier that matches the specified description, or creates a new one if none diff --git a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphCompiler.cs b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphCompiler.cs index f163836..232f948 100644 --- a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphCompiler.cs +++ b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphCompiler.cs @@ -212,10 +212,10 @@ internal sealed class RenderGraphCompiler continue; } - _graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource); + _graphicsEngine.ResourceDatabase.ScheduleReleaseResource(res.backingResource); } - _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); + _graphicsEngine.ResourceDatabase.ScheduleReleaseResource(_resourceHeap); } if (_aliasingManager.Heap.size == 0) @@ -380,11 +380,11 @@ internal sealed class RenderGraphCompiler { if (!res.isImported) { - _graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource); + _graphicsEngine.ResourceDatabase.ScheduleReleaseResource(res.backingResource); } } - _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); + _graphicsEngine.ResourceDatabase.ScheduleReleaseResource(_resourceHeap); _resourceHeap = Handle.Invalid; } } diff --git a/src/Runtime/Ghost.Graphics/RenderPasses/MeshRenderPass.cs b/src/Runtime/Ghost.Graphics/RenderPasses/MeshRenderPass.cs index 6d63c98..ce2cedb 100644 --- a/src/Runtime/Ghost.Graphics/RenderPasses/MeshRenderPass.cs +++ b/src/Runtime/Ghost.Graphics/RenderPasses/MeshRenderPass.cs @@ -321,7 +321,7 @@ internal class MeshRenderPass : IRenderPass { foreach (var texture in _textures) { - resourceDatabase.ReleaseResource(texture.AsResource()); + resourceDatabase.ScheduleReleaseResource(texture.AsResource()); } } } diff --git a/src/Runtime/Ghost.Graphics/RenderSystem.cs b/src/Runtime/Ghost.Graphics/RenderSystem.cs index 0688c5d..8adcfd4 100644 --- a/src/Runtime/Ghost.Graphics/RenderSystem.cs +++ b/src/Runtime/Ghost.Graphics/RenderSystem.cs @@ -265,7 +265,6 @@ internal class RenderSystem : IRenderSystem // Sync the current frame resource to this new fence to keep state consistent frameResource.FenceValue = flushFence; - foreach (var resource in _frameResources) { resource.CommandAllocator.Reset(); diff --git a/src/Runtime/Ghost.Graphics/Shaders/Blit.gshdr b/src/Runtime/Ghost.Graphics/Shaders/Blit.gshdr index 6e7f9aa..424b2e1 100644 --- a/src/Runtime/Ghost.Graphics/Shaders/Blit.gshdr +++ b/src/Runtime/Ghost.Graphics/Shaders/Blit.gshdr @@ -19,9 +19,9 @@ shader "Hidden/Blit" includes { - "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.hlsl"; - "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Color.hlsl"; - "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Properties.hlsl"; + "F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Common.hlsl"; + "F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Color.hlsl"; + "F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl"; } hlsl diff --git a/src/Runtime/Ghost.Graphics/runtime/win-x64/native/dxcompiler.dll b/src/Runtime/Ghost.Graphics/runtimes/win-x64/native/dxcompiler.dll similarity index 100% rename from src/Runtime/Ghost.Graphics/runtime/win-x64/native/dxcompiler.dll rename to src/Runtime/Ghost.Graphics/runtimes/win-x64/native/dxcompiler.dll diff --git a/src/Runtime/Ghost.Graphics/runtime/win-x64/native/dxil.dll b/src/Runtime/Ghost.Graphics/runtimes/win-x64/native/dxil.dll similarity index 100% rename from src/Runtime/Ghost.Graphics/runtime/win-x64/native/dxil.dll rename to src/Runtime/Ghost.Graphics/runtimes/win-x64/native/dxil.dll diff --git a/src/Test/Ghost.Entities.Test/QueryBenchmark.cs b/src/Test/Ghost.Entities.Test/QueryBenchmark.cs index 4fdf9a6..78367a7 100644 --- a/src/Test/Ghost.Entities.Test/QueryBenchmark.cs +++ b/src/Test/Ghost.Entities.Test/QueryBenchmark.cs @@ -5,7 +5,6 @@ using Misaki.HighPerformance.LowLevel.Buffer; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; namespace Ghost.Entities.Test; @@ -66,8 +65,6 @@ public class QueryBenchmark public void QueryEntities() { ref var query = ref _world.ComponentManager.GetEntityQueryReference(_queryIdentifier); - var vecDT = Vector256.Create(_dt); - foreach (var chunkView in query.GetChunkIterator()) { var positions = chunkView.GetComponentDataRW(); diff --git a/src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs b/src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs index 64d234c..860730b 100644 --- a/src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs +++ b/src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs @@ -2,7 +2,6 @@ using Ghost.Core; using Ghost.Graphics.Test.Windows; using Microsoft.UI.Xaml; -using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; using System.Runtime.InteropServices; // To learn more about WinUI, the WinUI project structure, @@ -32,7 +31,7 @@ public partial class UnitTestApp : Application OperatingSystem.IsLinux() ? "linux" : OperatingSystem.IsMacOS() ? "osx" : "unknown"; var arch = Environment.Is64BitProcess ? "x64" : "x86"; - var nativeDllDir = Path.Combine(currentDir, "runtime", platform + "-" + arch, "native"); + var nativeDllDir = Path.Combine(currentDir, "runtimes", platform + "-" + arch, "native"); if (Directory.Exists(nativeDllDir)) { foreach (var dll in Directory.EnumerateFiles(nativeDllDir, "*.dll")) diff --git a/src/Test/Ghost.MicroTest/Ghost.MicroTest.csproj b/src/Test/Ghost.MicroTest/Ghost.MicroTest.csproj index 3d9f42e..2f552f8 100644 --- a/src/Test/Ghost.MicroTest/Ghost.MicroTest.csproj +++ b/src/Test/Ghost.MicroTest/Ghost.MicroTest.csproj @@ -5,6 +5,7 @@ net10.0 enable enable + true @@ -13,6 +14,7 @@ + diff --git a/src/Test/Ghost.MicroTest/NvttBindingTest.cs b/src/Test/Ghost.MicroTest/NvttBindingTest.cs new file mode 100644 index 0000000..eeb171c --- /dev/null +++ b/src/Test/Ghost.MicroTest/NvttBindingTest.cs @@ -0,0 +1,206 @@ +using Ghost.Nvtt; +using Ghost.Nvtt.Native; +using Ghost.Test.Core; + +namespace Ghost.MicroTest; + +/// +/// 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. +/// +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; +} diff --git a/src/Test/Ghost.MicroTest/Program.cs b/src/Test/Ghost.MicroTest/Program.cs index 3751555..d72c3ad 100644 --- a/src/Test/Ghost.MicroTest/Program.cs +++ b/src/Test/Ghost.MicroTest/Program.cs @@ -1,2 +1,4 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using Ghost.Test.Core; +using Ghost.MicroTest; + +TestRunner.Run(); diff --git a/src/ThridParty/Ghost.FMOD/Ghost.FMOD.csproj b/src/ThridParty/Ghost.FMOD/Ghost.FMOD.csproj index 3f024e8..cdb3e27 100644 --- a/src/ThridParty/Ghost.FMOD/Ghost.FMOD.csproj +++ b/src/ThridParty/Ghost.FMOD/Ghost.FMOD.csproj @@ -15,17 +15,12 @@ - - - - - - + PreserveNewest - - + + PreserveNewest - + diff --git a/src/ThridParty/Ghost.FMOD/runtime/win-x64/native/fmod.dll b/src/ThridParty/Ghost.FMOD/runtimes/win-x64/native/fmod.dll similarity index 100% rename from src/ThridParty/Ghost.FMOD/runtime/win-x64/native/fmod.dll rename to src/ThridParty/Ghost.FMOD/runtimes/win-x64/native/fmod.dll diff --git a/src/ThridParty/Ghost.FMOD/runtime/win-x64/native/fmodstudio.dll b/src/ThridParty/Ghost.FMOD/runtimes/win-x64/native/fmodstudio.dll similarity index 100% rename from src/ThridParty/Ghost.FMOD/runtime/win-x64/native/fmodstudio.dll rename to src/ThridParty/Ghost.FMOD/runtimes/win-x64/native/fmodstudio.dll diff --git a/src/ThridParty/Ghost.MeshOptimizer/Ghost.MeshOptimizer.csproj b/src/ThridParty/Ghost.MeshOptimizer/Ghost.MeshOptimizer.csproj index 2bf805d..22722a5 100644 --- a/src/ThridParty/Ghost.MeshOptimizer/Ghost.MeshOptimizer.csproj +++ b/src/ThridParty/Ghost.MeshOptimizer/Ghost.MeshOptimizer.csproj @@ -16,9 +16,9 @@ - + PreserveNewest - + diff --git a/src/ThridParty/Ghost.MeshOptimizer/runtime/win-x64/native/meshoptimizer.dll b/src/ThridParty/Ghost.MeshOptimizer/runtimes/win-x64/native/meshoptimizer.dll similarity index 100% rename from src/ThridParty/Ghost.MeshOptimizer/runtime/win-x64/native/meshoptimizer.dll rename to src/ThridParty/Ghost.MeshOptimizer/runtimes/win-x64/native/meshoptimizer.dll diff --git a/src/ThridParty/Ghost.Nvtt/Ghost.Nvtt.csproj b/src/ThridParty/Ghost.Nvtt/Ghost.Nvtt.csproj index 5f1f7d8..dd97e4b 100644 --- a/src/ThridParty/Ghost.Nvtt/Ghost.Nvtt.csproj +++ b/src/ThridParty/Ghost.Nvtt/Ghost.Nvtt.csproj @@ -16,13 +16,13 @@ - + - + PreserveNewest - + diff --git a/src/ThridParty/Ghost.Nvtt/NVTT_USAGE.md b/src/ThridParty/Ghost.Nvtt/NVTT_USAGE.md new file mode 100644 index 0000000..8988e0e --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/NVTT_USAGE.md @@ -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(); + +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 +var outOptsList = new List(); + +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 | diff --git a/src/ThridParty/Ghost.Nvtt/Api.cs b/src/ThridParty/Ghost.Nvtt/Native/Api.cs similarity index 99% rename from src/ThridParty/Ghost.Nvtt/Api.cs rename to src/ThridParty/Ghost.Nvtt/Native/Api.cs index 4a5fb1d..cacfc16 100644 --- a/src/ThridParty/Ghost.Nvtt/Api.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/Api.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public static unsafe partial class Api { diff --git a/src/ThridParty/Ghost.Nvtt/EdgeFixup.cs b/src/ThridParty/Ghost.Nvtt/Native/EdgeFixup.cs similarity index 85% rename from src/ThridParty/Ghost.Nvtt/EdgeFixup.cs rename to src/ThridParty/Ghost.Nvtt/Native/EdgeFixup.cs index 8c0cd2e..377fe8d 100644 --- a/src/ThridParty/Ghost.Nvtt/EdgeFixup.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/EdgeFixup.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum EdgeFixup { diff --git a/src/ThridParty/Ghost.Nvtt/NativeAnnotationAttribute.cs b/src/ThridParty/Ghost.Nvtt/Native/NativeAnnotationAttribute.cs similarity index 97% rename from src/ThridParty/Ghost.Nvtt/NativeAnnotationAttribute.cs rename to src/ThridParty/Ghost.Nvtt/Native/NativeAnnotationAttribute.cs index 3703639..056449f 100644 --- a/src/ThridParty/Ghost.Nvtt/NativeAnnotationAttribute.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NativeAnnotationAttribute.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { /// Defines the annotation found in a native declaration. [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] diff --git a/src/ThridParty/Ghost.Nvtt/NativeTypeNameAttribute.cs b/src/ThridParty/Ghost.Nvtt/Native/NativeTypeNameAttribute.cs similarity index 97% rename from src/ThridParty/Ghost.Nvtt/NativeTypeNameAttribute.cs rename to src/ThridParty/Ghost.Nvtt/Native/NativeTypeNameAttribute.cs index df66412..911eac8 100644 --- a/src/ThridParty/Ghost.Nvtt/NativeTypeNameAttribute.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NativeTypeNameAttribute.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { /// Defines the type of a member as it was used in the native signature. [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = false, Inherited = true)] diff --git a/src/ThridParty/Ghost.Nvtt/NvttAlphaMode.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttAlphaMode.cs similarity index 84% rename from src/ThridParty/Ghost.Nvtt/NvttAlphaMode.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttAlphaMode.cs index 15af114..c618d86 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttAlphaMode.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttAlphaMode.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttAlphaMode { diff --git a/src/ThridParty/Ghost.Nvtt/Native/NvttBatchList.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttBatchList.cs new file mode 100644 index 0000000..0f9a8bb --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttBatchList.cs @@ -0,0 +1,6 @@ +namespace Ghost.Nvtt.Native +{ + public partial struct NvttBatchList + { + } +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttBoolean.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttBoolean.cs similarity index 74% rename from src/ThridParty/Ghost.Nvtt/NvttBoolean.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttBoolean.cs index cb19b8a..8a6da2a 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttBoolean.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttBoolean.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttBoolean { diff --git a/src/ThridParty/Ghost.Nvtt/NvttCPUInputBuffer.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttCPUInputBuffer.cs similarity index 68% rename from src/ThridParty/Ghost.Nvtt/NvttCPUInputBuffer.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttCPUInputBuffer.cs index f1e3557..2dbf605 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttCPUInputBuffer.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttCPUInputBuffer.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public partial struct NvttCPUInputBuffer { diff --git a/src/ThridParty/Ghost.Nvtt/NvttChannelOrder.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttChannelOrder.cs similarity index 91% rename from src/ThridParty/Ghost.Nvtt/NvttChannelOrder.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttChannelOrder.cs index 614a33e..c7b9dc0 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttChannelOrder.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttChannelOrder.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttChannelOrder { diff --git a/src/ThridParty/Ghost.Nvtt/Native/NvttCompressionOptions.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttCompressionOptions.cs new file mode 100644 index 0000000..74f407a --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttCompressionOptions.cs @@ -0,0 +1,6 @@ +namespace Ghost.Nvtt.Native +{ + public partial struct NvttCompressionOptions + { + } +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttContainer.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttContainer.cs similarity index 78% rename from src/ThridParty/Ghost.Nvtt/NvttContainer.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttContainer.cs index 1dbca1d..57fd5e5 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttContainer.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttContainer.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttContainer { diff --git a/src/ThridParty/Ghost.Nvtt/Native/NvttContext.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttContext.cs new file mode 100644 index 0000000..6fe4390 --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttContext.cs @@ -0,0 +1,6 @@ +namespace Ghost.Nvtt.Native +{ + public partial struct NvttContext + { + } +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttCubeLayout.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttCubeLayout.cs similarity index 89% rename from src/ThridParty/Ghost.Nvtt/NvttCubeLayout.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttCubeLayout.cs index 20f054e..49048e2 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttCubeLayout.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttCubeLayout.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttCubeLayout { diff --git a/src/ThridParty/Ghost.Nvtt/Native/NvttCubeSurface.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttCubeSurface.cs new file mode 100644 index 0000000..a389609 --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttCubeSurface.cs @@ -0,0 +1,6 @@ +namespace Ghost.Nvtt.Native +{ + public partial struct NvttCubeSurface + { + } +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttEncodeFlags.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttEncodeFlags.cs similarity index 88% rename from src/ThridParty/Ghost.Nvtt/NvttEncodeFlags.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttEncodeFlags.cs index a5e712a..c52040d 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttEncodeFlags.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttEncodeFlags.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttEncodeFlags { diff --git a/src/ThridParty/Ghost.Nvtt/NvttEncodeSettings.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttEncodeSettings.cs similarity index 93% rename from src/ThridParty/Ghost.Nvtt/NvttEncodeSettings.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttEncodeSettings.cs index 956b027..a3188e3 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttEncodeSettings.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttEncodeSettings.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public unsafe partial struct NvttEncodeSettings { diff --git a/src/ThridParty/Ghost.Nvtt/NvttError.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttError.cs similarity index 94% rename from src/ThridParty/Ghost.Nvtt/NvttError.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttError.cs index af3e275..41e6de5 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttError.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttError.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttError { diff --git a/src/ThridParty/Ghost.Nvtt/NvttFormat.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttFormat.cs similarity index 97% rename from src/ThridParty/Ghost.Nvtt/NvttFormat.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttFormat.cs index 93540a8..1250983 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttFormat.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttFormat.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttFormat { diff --git a/src/ThridParty/Ghost.Nvtt/NvttGPUInputBuffer.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttGPUInputBuffer.cs similarity index 68% rename from src/ThridParty/Ghost.Nvtt/NvttGPUInputBuffer.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttGPUInputBuffer.cs index b4ee839..c8b5e3c 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttGPUInputBuffer.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttGPUInputBuffer.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public partial struct NvttGPUInputBuffer { diff --git a/src/ThridParty/Ghost.Nvtt/NvttInputFormat.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttInputFormat.cs similarity index 88% rename from src/ThridParty/Ghost.Nvtt/NvttInputFormat.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttInputFormat.cs index 1bb3559..504ff3e 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttInputFormat.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttInputFormat.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttInputFormat { diff --git a/src/ThridParty/Ghost.Nvtt/NvttMipmapFilter.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttMipmapFilter.cs similarity index 89% rename from src/ThridParty/Ghost.Nvtt/NvttMipmapFilter.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttMipmapFilter.cs index eb7fc9f..b1dde9f 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttMipmapFilter.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttMipmapFilter.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttMipmapFilter { diff --git a/src/ThridParty/Ghost.Nvtt/NvttNormalTransform.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttNormalTransform.cs similarity index 88% rename from src/ThridParty/Ghost.Nvtt/NvttNormalTransform.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttNormalTransform.cs index bed3671..8f0423b 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttNormalTransform.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttNormalTransform.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttNormalTransform { diff --git a/src/ThridParty/Ghost.Nvtt/Native/NvttOutputOptions.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttOutputOptions.cs new file mode 100644 index 0000000..4723924 --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttOutputOptions.cs @@ -0,0 +1,6 @@ +namespace Ghost.Nvtt.Native +{ + public partial struct NvttOutputOptions + { + } +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttPixelType.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttPixelType.cs similarity index 91% rename from src/ThridParty/Ghost.Nvtt/NvttPixelType.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttPixelType.cs index 5636ca5..67a6fb8 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttPixelType.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttPixelType.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttPixelType { diff --git a/src/ThridParty/Ghost.Nvtt/NvttQuality.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttQuality.cs similarity index 85% rename from src/ThridParty/Ghost.Nvtt/NvttQuality.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttQuality.cs index 7333169..a6437a4 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttQuality.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttQuality.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttQuality { diff --git a/src/ThridParty/Ghost.Nvtt/NvttRefImage.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttRefImage.cs similarity index 95% rename from src/ThridParty/Ghost.Nvtt/NvttRefImage.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttRefImage.cs index 8364e0a..2a37863 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttRefImage.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttRefImage.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public unsafe partial struct NvttRefImage { diff --git a/src/ThridParty/Ghost.Nvtt/NvttResizeFilter.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttResizeFilter.cs similarity index 89% rename from src/ThridParty/Ghost.Nvtt/NvttResizeFilter.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttResizeFilter.cs index 737a577..e4bb625 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttResizeFilter.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttResizeFilter.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttResizeFilter { diff --git a/src/ThridParty/Ghost.Nvtt/NvttRoundMode.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttRoundMode.cs similarity index 87% rename from src/ThridParty/Ghost.Nvtt/NvttRoundMode.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttRoundMode.cs index d086297..fb62db7 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttRoundMode.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttRoundMode.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttRoundMode { diff --git a/src/ThridParty/Ghost.Nvtt/NvttSeverity.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttSeverity.cs similarity index 85% rename from src/ThridParty/Ghost.Nvtt/NvttSeverity.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttSeverity.cs index 22fbadd..459a0b6 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttSeverity.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttSeverity.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttSeverity { diff --git a/src/ThridParty/Ghost.Nvtt/Native/NvttSurface.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttSurface.cs new file mode 100644 index 0000000..70f9783 --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttSurface.cs @@ -0,0 +1,6 @@ +namespace Ghost.Nvtt.Native +{ + public partial struct NvttSurface + { + } +} diff --git a/src/ThridParty/Ghost.Nvtt/Native/NvttSurfaceSet.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttSurfaceSet.cs new file mode 100644 index 0000000..4e719bc --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttSurfaceSet.cs @@ -0,0 +1,6 @@ +namespace Ghost.Nvtt.Native +{ + public partial struct NvttSurfaceSet + { + } +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttTextureType.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttTextureType.cs similarity index 83% rename from src/ThridParty/Ghost.Nvtt/NvttTextureType.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttTextureType.cs index 2abdcf7..b45567e 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttTextureType.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttTextureType.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttTextureType { diff --git a/src/ThridParty/Ghost.Nvtt/Native/NvttTimingContext.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttTimingContext.cs new file mode 100644 index 0000000..31fa956 --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttTimingContext.cs @@ -0,0 +1,6 @@ +namespace Ghost.Nvtt.Native +{ + public partial struct NvttTimingContext + { + } +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttToneMapper.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttToneMapper.cs similarity index 89% rename from src/ThridParty/Ghost.Nvtt/NvttToneMapper.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttToneMapper.cs index 60c0b7a..ff5c901 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttToneMapper.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttToneMapper.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttToneMapper { diff --git a/src/ThridParty/Ghost.Nvtt/NvttValueType.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttValueType.cs similarity index 83% rename from src/ThridParty/Ghost.Nvtt/NvttValueType.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttValueType.cs index 41135de..c6a1570 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttValueType.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttValueType.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttValueType { diff --git a/src/ThridParty/Ghost.Nvtt/NvttWrapMode.cs b/src/ThridParty/Ghost.Nvtt/Native/NvttWrapMode.cs similarity index 82% rename from src/ThridParty/Ghost.Nvtt/NvttWrapMode.cs rename to src/ThridParty/Ghost.Nvtt/Native/NvttWrapMode.cs index ee5dd35..bf857ad 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttWrapMode.cs +++ b/src/ThridParty/Ghost.Nvtt/Native/NvttWrapMode.cs @@ -1,4 +1,4 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt.Native { public enum NvttWrapMode { diff --git a/src/ThridParty/Ghost.Nvtt/NvttBatchList.cs b/src/ThridParty/Ghost.Nvtt/NvttBatchList.cs index b0ddda4..57fe86a 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttBatchList.cs +++ b/src/ThridParty/Ghost.Nvtt/NvttBatchList.cs @@ -1,6 +1,91 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt; + +/// +/// Wrapper around an nvtt batch list — a list of (surface, face, mipmap, +/// outputOptions) tuples passed to . +/// +public sealed unsafe class NvttBatchListHandle : IDisposable { - public partial struct NvttBatchList + private NvttBatchList* _ptr; + + /// Raw pointer – use only when calling the native API directly. + public NvttBatchList* Ptr => _ptr; + + // ------------------------------------------------------------------------- + // Construction / destruction + // ------------------------------------------------------------------------- + + public NvttBatchListHandle() => _ptr = Api.nvttCreateBatchList(); + + public void Dispose() { + if (_ptr != null) + { + Api.nvttDestroyBatchList(_ptr); + _ptr = null; + } + } + + // ------------------------------------------------------------------------- + // Mutation + // ------------------------------------------------------------------------- + + /// Removes all items from the list. + public void Clear() + { + ThrowIfDisposed(); + Api.nvttBatchListClear(_ptr); + } + + /// + /// Appends an entry. The and + /// must remain alive for the duration of + /// any subsequent call. + /// + public void Append(NvttSurfaceHandle surface, int face, int mipmap, + NvttOutputOptionsHandle outputOptions) + { + ThrowIfDisposed(); + Api.nvttBatchListAppend(_ptr, surface.Ptr, face, mipmap, + outputOptions.Ptr); + } + + // ------------------------------------------------------------------------- + // Query + // ------------------------------------------------------------------------- + + /// Number of items currently in the list. + public uint Count + { + get { ThrowIfDisposed(); return Api.nvttBatchListGetSize(_ptr); } + } + + /// + /// Returns the raw pointers for item . + /// The pointers are borrowed – do NOT dispose them. + /// + 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)); + } } } diff --git a/src/ThridParty/Ghost.Nvtt/NvttCompressionOptions.cs b/src/ThridParty/Ghost.Nvtt/NvttCompressionOptions.cs index 723d1f7..3ecba6f 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttCompressionOptions.cs +++ b/src/ThridParty/Ghost.Nvtt/NvttCompressionOptions.cs @@ -1,6 +1,123 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt; + +/// +/// Controls how a surface is compressed – format, quality, pixel layout and +/// optional quantization settings. +/// +public sealed unsafe class NvttCompressionOptionsHandle : IDisposable { - public partial struct NvttCompressionOptions + private NvttCompressionOptions* _ptr; + + /// Raw pointer – use only when calling the native API directly. + public NvttCompressionOptions* Ptr => _ptr; + + // ------------------------------------------------------------------------- + // Construction / destruction + // ------------------------------------------------------------------------- + + public NvttCompressionOptionsHandle() => _ptr = Api.nvttCreateCompressionOptions(); + + public void Dispose() { + if (_ptr != null) + { + Api.nvttDestroyCompressionOptions(_ptr); + _ptr = null; + } + } + + // ------------------------------------------------------------------------- + // Properties + // ------------------------------------------------------------------------- + + /// Target compressed format (e.g. BC1, BC7, ASTC …). + public NvttFormat Format + { + set { ThrowIfDisposed(); Api.nvttSetCompressionOptionsFormat(_ptr, value); } + } + + /// Compression quality preset. + public NvttQuality Quality + { + set { ThrowIfDisposed(); Api.nvttSetCompressionOptionsQuality(_ptr, value); } + } + + /// Pixel type for uncompressed RGB(A) output. + public NvttPixelType PixelType + { + set { ThrowIfDisposed(); Api.nvttSetCompressionOptionsPixelType(_ptr, value); } + } + + /// Row-pitch alignment in bytes for uncompressed output. + public int PitchAlignment + { + set { ThrowIfDisposed(); Api.nvttSetCompressionOptionsPitchAlignment(_ptr, value); } + } + + // ------------------------------------------------------------------------- + // Methods + // ------------------------------------------------------------------------- + + /// Resets all options to their default values. + public void Reset() + { + ThrowIfDisposed(); + Api.nvttResetCompressionOptions(_ptr); + } + + /// + /// Sets per-channel importance weights used during block-compression error + /// minimisation. + /// + public void SetColorWeights(float red, float green, float blue, float alpha = 1f) + { + ThrowIfDisposed(); + Api.nvttSetCompressionOptionsColorWeights(_ptr, red, green, blue, alpha); + } + + /// + /// Configures a custom uncompressed pixel format by specifying the bit-depth + /// and per-channel bit masks. + /// + public void SetPixelFormat(uint bitCount, uint rMask, uint gMask, uint bMask, uint aMask) + { + ThrowIfDisposed(); + Api.nvttSetCompressionOptionsPixelFormat(_ptr, bitCount, rMask, gMask, bMask, aMask); + } + + /// + /// Enables or disables dithering and binary-alpha quantisation. + /// + /// Dither RGB channels. + /// Dither the alpha channel. + /// Snap alpha to 0 or 255. + /// Threshold used when is true. + 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); + } + + /// Returns the D3D9 FourCC / D3DFORMAT value for the current settings. + public uint GetD3D9Format() + { + ThrowIfDisposed(); + return Api.nvttGetCompressionOptionsD3D9Format(_ptr); + } + + // ------------------------------------------------------------------------- + + private void ThrowIfDisposed() + { + if (_ptr == null) + { + throw new ObjectDisposedException(nameof(NvttCompressionOptionsHandle)); + } } } diff --git a/src/ThridParty/Ghost.Nvtt/NvttContext.cs b/src/ThridParty/Ghost.Nvtt/NvttContext.cs index f96b819..38b677e 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttContext.cs +++ b/src/ThridParty/Ghost.Nvtt/NvttContext.cs @@ -1,6 +1,229 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt; + +/// +/// Wrapper around the nvtt compression context — the central object that drives +/// the compression pipeline. +/// +public sealed unsafe class NvttContextHandle : IDisposable { - public partial struct NvttContext + private NvttContext* _ptr; + + /// Raw pointer – use only when calling the native API directly. + public NvttContext* Ptr => _ptr; + + // ------------------------------------------------------------------------- + // Construction / destruction + // ------------------------------------------------------------------------- + + public NvttContextHandle() => _ptr = Api.nvttCreateContext(); + + public void Dispose() { + if (_ptr != null) + { + Api.nvttDestroyContext(_ptr); + _ptr = null; + } + } + + // ------------------------------------------------------------------------- + // CUDA acceleration + // ------------------------------------------------------------------------- + + /// Enables or disables CUDA-accelerated compression. + public void SetCudaAcceleration(bool enable) + { + ThrowIfDisposed(); + Api.nvttSetContextCudaAcceleration(_ptr, NvttInterop.ToNvtt(enable)); + } + + /// Returns true if CUDA acceleration is currently enabled. + public bool IsCudaAccelerationEnabled + { + get + { + ThrowIfDisposed(); + return NvttInterop.ToBool(Api.nvttContextIsCudaAccelerationEnabled(_ptr)); + } + } + + // ------------------------------------------------------------------------- + // Timing + // ------------------------------------------------------------------------- + + /// + /// Enables or disables internal timing collection. + /// controls the granularity (0 = off, higher = more detail). + /// + public void EnableTiming(bool enable, int detailLevel = 1) + { + ThrowIfDisposed(); + Api.nvttContextEnableTiming(_ptr, NvttInterop.ToNvtt(enable), detailLevel); + } + + /// + /// Returns the timing context owned by this nvtt context. + /// The pointer is borrowed – do NOT dispose it separately. + /// Returns null if timing was never enabled. + /// + public NvttTimingContext* GetTimingContextPtr() + { + ThrowIfDisposed(); + return Api.nvttContextGetTimingContext(_ptr); + } + + // ------------------------------------------------------------------------- + // Estimate size + // ------------------------------------------------------------------------- + + /// + /// Estimates the compressed size in bytes for + /// mip levels of using . + /// + public int EstimateSize(NvttSurfaceHandle img, int mipmapCount, + NvttCompressionOptionsHandle compressionOptions) + { + ThrowIfDisposed(); + return Api.nvttContextEstimateSize(_ptr, img.Ptr, mipmapCount, + compressionOptions.Ptr); + } + + /// Estimates the compressed size for a cube map. + public int EstimateSizeCube(NvttCubeSurfaceHandle img, int mipmapCount, + NvttCompressionOptionsHandle compressionOptions) + { + ThrowIfDisposed(); + return Api.nvttContextEstimateSizeCube(_ptr, img.Ptr, mipmapCount, + compressionOptions.Ptr); + } + + /// Estimates the compressed size for raw-data dimensions. + 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 + // ------------------------------------------------------------------------- + + /// + /// Writes the DDS / KTX header to . + /// Must be called once before compressing mip levels. + /// Returns false on failure. + /// + 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)); + } + + /// Writes the header for a cube-map texture. + 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)); + } + + /// Writes the header using explicit dimensions instead of a surface. + 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 + // ------------------------------------------------------------------------- + + /// + /// Compresses a single face/mip of and sends the + /// result to . + /// Returns false on failure. + /// + 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)); + } + + /// Compresses a single mip of a cube-map face. + 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)); + } + + /// Compresses a single mip from a raw float RGBA buffer. + public bool CompressData(int w, int h, int d, int face, int mipmap, + ReadOnlySpan 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)); + } + } + + /// + /// Compresses a batch of (surface, face, mipmap, outputOptions) entries + /// using the shared . + /// Returns false on failure. + /// + public bool CompressBatch(NvttBatchListHandle batchList, + NvttCompressionOptionsHandle compressionOptions) + { + ThrowIfDisposed(); + return NvttInterop.ToBool( + Api.nvttContextCompressBatch(_ptr, batchList.Ptr, + compressionOptions.Ptr)); + } + + // ------------------------------------------------------------------------- + // Quantize + // ------------------------------------------------------------------------- + + /// + /// Quantizes in place according to + /// (useful before compressing + /// to formats that only support limited bit depths). + /// + 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)); + } } } diff --git a/src/ThridParty/Ghost.Nvtt/NvttCubeSurface.cs b/src/ThridParty/Ghost.Nvtt/NvttCubeSurface.cs index 8728c8f..4ccd295 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttCubeSurface.cs +++ b/src/ThridParty/Ghost.Nvtt/NvttCubeSurface.cs @@ -1,6 +1,227 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt; + +/// +/// Wrapper around an nvtt cube-map surface (six faces, optional mip chain). +/// +/// Methods that return a new transfer ownership +/// to the caller; dispose the result when done. +/// +public sealed unsafe class NvttCubeSurfaceHandle : IDisposable { - public partial struct NvttCubeSurface + private NvttCubeSurface* _ptr; + + /// Raw pointer – use only when calling the native API directly. + public NvttCubeSurface* Ptr => _ptr; + + // ------------------------------------------------------------------------- + // Construction / destruction + // ------------------------------------------------------------------------- + + public NvttCubeSurfaceHandle() => _ptr = Api.nvttCreateCubeSurface(); + + /// Wraps an existing raw pointer (takes ownership; will destroy on dispose). + internal NvttCubeSurfaceHandle(NvttCubeSurface* existing) => _ptr = existing; + + public void Dispose() { + if (_ptr != null) + { + Api.nvttDestroyCubeSurface(_ptr); + _ptr = null; + } + } + + // ------------------------------------------------------------------------- + // Read-only properties + // ------------------------------------------------------------------------- + + /// Returns true when the cube surface holds no data. + public bool IsNull + { + get { ThrowIfDisposed(); return NvttInterop.ToBool(Api.nvttCubeSurfaceIsNull(_ptr)); } + } + + /// Side length in pixels of each face. + public int EdgeLength + { + get { ThrowIfDisposed(); return Api.nvttCubeSurfaceEdgeLength(_ptr); } + } + + /// Number of mip levels stored in this cube surface. + public int MipmapCount + { + get { ThrowIfDisposed(); return Api.nvttCubeSurfaceCountMipmaps(_ptr); } + } + + // ------------------------------------------------------------------------- + // Load / Save + // ------------------------------------------------------------------------- + + /// + /// Loads a cube map from disk. + /// selects which mip level to load (-1 = all). + /// Returns false on failure. + /// + public bool Load(string fileName, int mipmap = 0) + { + ThrowIfDisposed(); + Span 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)); + } + } + + /// Loads a cube map from a managed byte array. Returns false on failure. + public bool LoadFromMemory(ReadOnlySpan data, int mipmap = 0) + { + ThrowIfDisposed(); + fixed (byte* p = data) + { + return NvttInterop.ToBool( + Api.nvttCubeSurfaceLoadFromMemory(_ptr, p, (ulong)data.Length, mipmap)); + } + } + + /// Saves the cube map to disk. Returns false on failure. + public bool Save(string fileName) + { + ThrowIfDisposed(); + Span 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 + // ------------------------------------------------------------------------- + + /// + /// Returns the raw pointer for the given face + /// (0–5). The pointer is owned by this cube surface – do NOT dispose it. + /// + public NvttSurface* FacePtr(int face) + { + ThrowIfDisposed(); + return Api.nvttCubeSurfaceFace(_ptr, face); + } + + // ------------------------------------------------------------------------- + // Fold / Unfold + // ------------------------------------------------------------------------- + + /// + /// Folds a cross-layout into this cube surface. + /// + public void Fold(NvttSurfaceHandle img, NvttCubeLayout layout) + { + ThrowIfDisposed(); + Api.nvttCubeSurfaceFold(_ptr, img.Ptr, layout); + } + + /// + /// Unfolds the cube surface into a flat cross-layout image. + /// Caller owns the returned surface. + /// + public NvttSurfaceHandle Unfold(NvttCubeLayout layout) + { + ThrowIfDisposed(); + return new NvttSurfaceHandle(Api.nvttCubeSurfaceUnfold(_ptr, layout)); + } + + // ------------------------------------------------------------------------- + // Query methods + // ------------------------------------------------------------------------- + + /// Returns the per-channel average for the given channel index. + public float Average(int channel) + { + ThrowIfDisposed(); + return Api.nvttCubeSurfaceAverage(_ptr, channel); + } + + /// Returns the min and max values of a channel across all faces. + 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 + // ------------------------------------------------------------------------- + + /// Clamps a channel to [, ]. + public void Clamp(int channel, float low, float high) + { + ThrowIfDisposed(); + Api.nvttCubeSurfaceClamp(_ptr, channel, low, high); + } + + /// Applies gamma expansion (toLinear) to all faces. + public void ToLinear(float gamma) + { + ThrowIfDisposed(); + Api.nvttCubeSurfaceToLinear(_ptr, gamma); + } + + /// Applies gamma compression to all faces. + public void ToGamma(float gamma) + { + ThrowIfDisposed(); + Api.nvttCubeSurfaceToGamma(_ptr, gamma); + } + + // ------------------------------------------------------------------------- + // Filtering (return new owned NvttCubeSurface) + // ------------------------------------------------------------------------- + + /// + /// Computes an irradiance-filtered cube map of the given . + /// Caller owns the result. + /// + public NvttCubeSurfaceHandle IrradianceFilter(int size, EdgeFixup fixup = EdgeFixup.NVTT_EdgeFixup_None) + { + ThrowIfDisposed(); + return new NvttCubeSurfaceHandle(Api.nvttCubeSurfaceIrradianceFilter(_ptr, size, fixup)); + } + + /// + /// Computes a cosine-power (specular) filtered cube map. + /// Caller owns the result. + /// + public NvttCubeSurfaceHandle CosinePowerFilter(int size, float cosinePower, + EdgeFixup fixup = EdgeFixup.NVTT_EdgeFixup_None) + { + ThrowIfDisposed(); + return new NvttCubeSurfaceHandle( + Api.nvttCubeSurfaceCosinePowerFilter(_ptr, size, cosinePower, fixup)); + } + + /// + /// Resamples the cube map to the given using fast bilinear resampling. + /// Caller owns the result. + /// + 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)); + } } } diff --git a/src/ThridParty/Ghost.Nvtt/NvttGlobal.cs b/src/ThridParty/Ghost.Nvtt/NvttGlobal.cs new file mode 100644 index 0000000..2f1bbb8 --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/NvttGlobal.cs @@ -0,0 +1,183 @@ +using System.Runtime.InteropServices; + +namespace Ghost.Nvtt; + +/// +/// Static helpers wrapping global nvtt functions (version, CUDA detection, +/// error utilities, image comparison, mipmap helpers). +/// +public static unsafe class NvttGlobal +{ + // ------------------------------------------------------------------------- + // Library info + // ------------------------------------------------------------------------- + + /// Returns the nvtt library version as a packed uint (major*10000 + minor*100 + patch). + public static uint Version => Api.nvttVersion(); + + /// Returns true when a CUDA-capable GPU is available. + public static bool IsCudaSupported + => NvttInterop.ToBool(Api.nvttIsCudaSupported()); + + // ------------------------------------------------------------------------- + // Error strings + // ------------------------------------------------------------------------- + + /// Returns a human-readable string for . + 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. + // ------------------------------------------------------------------------- + + /// + /// Delegate type for the global nvtt message callback. + /// + 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); + + /// + /// A registration token returned by . + /// Dispose to clear the callback and release the pinned delegate. + /// + 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; + } + } + } + + /// + /// Registers a managed message callback that nvtt calls for warnings and errors. + /// Returns a token; dispose the token to unregister. + /// + 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])fp, + null); + + return new MessageCallbackToken(native); + } + + // ------------------------------------------------------------------------- + // Image comparison (error metrics) + // ------------------------------------------------------------------------- + + /// RMS per-channel colour error between two surfaces. + public static float RmsError(NvttSurfaceHandle reference, NvttSurfaceHandle img, + NvttTimingContext* tc = null) + => Api.nvttRmsError(reference.Ptr, img.Ptr, tc); + + /// RMS alpha-channel error between two surfaces. + public static float RmsAlphaError(NvttSurfaceHandle reference, NvttSurfaceHandle img, + NvttTimingContext* tc = null) + => Api.nvttRmsAlphaError(reference.Ptr, img.Ptr, tc); + + /// RMS CIE-Lab perceptual error between two surfaces. + public static float RmsCIELabError(NvttSurfaceHandle reference, NvttSurfaceHandle img, + NvttTimingContext* tc = null) + => Api.nvttRmsCIELabError(reference.Ptr, img.Ptr, tc); + + /// Angular error between two normal-map surfaces. + public static float AngularError(NvttSurfaceHandle reference, NvttSurfaceHandle img, + NvttTimingContext* tc = null) + => Api.nvttAngularError(reference.Ptr, img.Ptr, tc); + + /// + /// Tone-mapped RMS error. Useful for HDR comparisons. + /// + public static float RmsToneMappedError(NvttSurfaceHandle reference, NvttSurfaceHandle img, + float exposure, NvttTimingContext* tc = null) + => Api.nvttRmsToneMappedError(reference.Ptr, img.Ptr, exposure, tc); + + // ------------------------------------------------------------------------- + // Difference image + // ------------------------------------------------------------------------- + + /// + /// Returns a new surface containing the scaled per-pixel difference. + /// Caller owns the returned surface. + /// + public static NvttSurfaceHandle Diff(NvttSurfaceHandle reference, NvttSurfaceHandle img, + float scale, NvttTimingContext* tc = null) + => new NvttSurfaceHandle(Api.nvttDiff(reference.Ptr, img.Ptr, scale, tc)); + + // ------------------------------------------------------------------------- + // Histogram + // ------------------------------------------------------------------------- + + /// + /// Returns a new surface containing a histogram visualisation of + /// at the given dimensions. + /// Caller owns the returned surface. + /// + public static NvttSurfaceHandle Histogram(NvttSurfaceHandle img, int width, int height, + NvttTimingContext* tc = null) + => new NvttSurfaceHandle(Api.nvttHistogram(img.Ptr, width, height, tc)); + + /// + /// Returns a new surface containing a histogram visualisation with an + /// explicit value range. + /// Caller owns the returned surface. + /// + 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 + // ------------------------------------------------------------------------- + + /// + /// Computes the target extent (power-of-two rounding, texture type clamping, + /// etc.) for a texture of the given dimensions. + /// Modifies , and + /// in place. + /// + 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); + } + } + + /// + /// Returns the number of mip levels that can be generated for the given + /// base dimensions. + /// + public static int CountMipmaps(int w, int h, int d, + NvttTimingContext* tc = null) + => Api.nvttCountMipmaps(w, h, d, tc); +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttInterop.cs b/src/ThridParty/Ghost.Nvtt/NvttInterop.cs new file mode 100644 index 0000000..d4bd94e --- /dev/null +++ b/src/ThridParty/Ghost.Nvtt/NvttInterop.cs @@ -0,0 +1,67 @@ +using System.Runtime.CompilerServices; +using System.Text; + +namespace Ghost.Nvtt; + +/// +/// Internal helpers for converting between managed and unmanaged types. +/// +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 overload lets the + // caller decide the stackalloc size. + // ------------------------------------------------------------------------- + + internal const int _MAX_STACK_PATH = 512; + + /// + /// Encode as null-terminated UTF-8 into + /// . Returns the used portion (including the + /// null terminator). If the buffer is too small a new heap array is + /// returned instead. + /// + internal static Span ToUtf8(string value, Span 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); + } +} diff --git a/src/ThridParty/Ghost.Nvtt/NvttOutputOptions.cs b/src/ThridParty/Ghost.Nvtt/NvttOutputOptions.cs index 567cff7..f0fa36e 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttOutputOptions.cs +++ b/src/ThridParty/Ghost.Nvtt/NvttOutputOptions.cs @@ -1,6 +1,211 @@ -namespace Ghost.Nvtt +using System.Runtime.InteropServices; + +namespace Ghost.Nvtt; + +/// +/// 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 . +/// The delegates are kept alive by pinned instances +/// for the lifetime of this object. +/// +public sealed unsafe class NvttOutputOptionsHandle : IDisposable { - public partial struct NvttOutputOptions + private NvttOutputOptions* _ptr; + + /// Raw pointer – use only when calling the native API directly. + 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? _beginImage; + private Func? _outputData; + private Action? _endImage; + private Action? _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 + // ------------------------------------------------------------------------- + + /// + /// Path of the output file. The file is created/overwritten when + /// compression runs. + /// + public string FileName + { + set + { + ThrowIfDisposed(); + Span buf = stackalloc byte[NvttInterop._MAX_STACK_PATH]; + var utf8 = NvttInterop.ToUtf8(value, buf); + fixed (byte* p = utf8) + { + Api.nvttSetOutputOptionsFileName(_ptr, (sbyte*)p); + } + } + } + + /// + /// Whether to write a DDS / KTX file header. Default is true. + /// + public bool OutputHeader + { + set { ThrowIfDisposed(); Api.nvttSetOutputOptionsOutputHeader(_ptr, NvttInterop.ToNvtt(value)); } + } + + /// Container format (DDS or DDS10). + public NvttContainer Container + { + set { ThrowIfDisposed(); Api.nvttSetOutputOptionsContainer(_ptr, value); } + } + + /// Application-defined version number stored in the header. + public int UserVersion + { + set { ThrowIfDisposed(); Api.nvttSetOutputOptionsUserVersion(_ptr, value); } + } + + /// Sets the sRGB flag in the output header. + public bool Srgb + { + set { ThrowIfDisposed(); Api.nvttSetOutputOptionsSrgbFlag(_ptr, NvttInterop.ToNvtt(value)); } + } + + // ------------------------------------------------------------------------- + // Methods + // ------------------------------------------------------------------------- + + /// Resets all options to their default values. + public void Reset() + { + ThrowIfDisposed(); + Api.nvttResetOutputOptions(_ptr); + } + + /// + /// Installs managed callbacks that receive the compressed data stream. + /// + /// is called once per mip level before any + /// data arrives: (size, width, height, depth, face, mipLevel). + /// receives each chunk of compressed bytes. + /// The first argument is an pointing to the data, the + /// second is the byte count. Return true to continue, false + /// to abort. + /// is called once after the last chunk. + /// + /// Only one set of output callbacks can be active at a time; calling this + /// method again replaces the previous ones. + /// + public void SetOutputHandler( + Action? beginImage, + Func? outputData, + Action? endImage) + { + ThrowIfDisposed(); + _beginImage = beginImage; + _outputData = outputData; + _endImage = endImage; + RebindOutputHandler(); + } + + /// + /// Installs a managed error handler. The handler is invoked with the + /// code whenever the native library encounters an + /// error. + /// + public void SetErrorHandler(Action? 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])beginPtr, + (delegate* unmanaged[Cdecl])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])errorPtr); + } + + // ------------------------------------------------------------------------- + + private void ThrowIfDisposed() + { + if (_ptr == null) + { + throw new ObjectDisposedException(nameof(NvttOutputOptionsHandle)); + } } } diff --git a/src/ThridParty/Ghost.Nvtt/NvttSurface.cs b/src/ThridParty/Ghost.Nvtt/NvttSurface.cs index b5dc73f..770a05d 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttSurface.cs +++ b/src/ThridParty/Ghost.Nvtt/NvttSurface.cs @@ -1,6 +1,715 @@ -namespace Ghost.Nvtt +using Ghost.Nvtt.Native; +using System.Runtime.InteropServices; + +namespace Ghost.Nvtt; + +/// +/// 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 timing +/// context. Pass null (the default) to skip timing. +/// +public sealed unsafe class NvttSurfaceHandle : IDisposable { - public partial struct NvttSurface + private NvttSurface* _ptr; + + /// Raw pointer – use only when calling the native API directly. + public NvttSurface* Ptr => _ptr; + + // ------------------------------------------------------------------------- + // Construction / destruction + // ------------------------------------------------------------------------- + + public NvttSurfaceHandle() => _ptr = Api.nvttCreateSurface(); + + /// Wraps an existing raw pointer (takes ownership; will destroy on dispose). + internal NvttSurfaceHandle(NvttSurface* existing) => _ptr = existing; + + public void Dispose() { + if (_ptr != null) + { + Api.nvttDestroySurface(_ptr); + _ptr = null; + } + } + + // ------------------------------------------------------------------------- + // Clone / sub-image + // ------------------------------------------------------------------------- + + /// Returns a deep copy of this surface. + public NvttSurfaceHandle Clone() + { + ThrowIfDisposed(); + return new NvttSurfaceHandle(Api.nvttSurfaceClone(_ptr)); + } + + /// + /// Extracts a rectangular sub-region into a new . + /// + 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 + // ------------------------------------------------------------------------- + + /// Returns true when the surface holds no data. + public bool IsNull + { + get { ThrowIfDisposed(); return NvttInterop.ToBool(Api.nvttSurfaceIsNull(_ptr)); } + } + + /// Image width in pixels. + public int Width + { + get { ThrowIfDisposed(); return Api.nvttSurfaceWidth(_ptr); } + } + + /// Image height in pixels. + public int Height + { + get { ThrowIfDisposed(); return Api.nvttSurfaceHeight(_ptr); } + } + + /// Image depth (1 for 2-D textures). + public int Depth + { + get { ThrowIfDisposed(); return Api.nvttSurfaceDepth(_ptr); } + } + + /// Texture dimensionality. + public NvttTextureType TextureType + { + get { ThrowIfDisposed(); return Api.nvttSurfaceType(_ptr); } + } + + /// Whether the surface contains a normal map. + public bool IsNormalMap + { + get { ThrowIfDisposed(); return NvttInterop.ToBool(Api.nvttSurfaceIsNormalMap(_ptr)); } + } + + // ------------------------------------------------------------------------- + // Settable properties + // ------------------------------------------------------------------------- + + /// UV wrap mode used when filtering near edges. + public NvttWrapMode WrapMode + { + get { ThrowIfDisposed(); return Api.nvttSurfaceWrapMode(_ptr); } + } + + /// Alpha mode interpretation. + public NvttAlphaMode AlphaMode + { + get { ThrowIfDisposed(); return Api.nvttSurfaceAlphaMode(_ptr); } + } + + // ------------------------------------------------------------------------- + // Query methods + // ------------------------------------------------------------------------- + + /// + /// Returns the number of mip levels that can be generated down to + /// pixels on the smallest side. + /// + public int CountMipmaps(int minSize = 1) + { + ThrowIfDisposed(); + return Api.nvttSurfaceCountMipmaps(_ptr, minSize); + } + + /// Alpha-test coverage for the given reference value and channel. + public float AlphaTestCoverage(float alphaRef, int alphaChannel = 3) + { + ThrowIfDisposed(); + return Api.nvttSurfaceAlphaTestCoverage(_ptr, alphaRef, alphaChannel); + } + + /// Per-channel average luminance. + public float Average(int channel, int alphaChannel = 3, float gamma = 2.2f) + { + ThrowIfDisposed(); + return Api.nvttSurfaceAverage(_ptr, channel, alphaChannel, gamma); + } + + /// + /// Returns a pointer to the raw float data for all four channels interleaved. + /// The span is valid only while this surface is alive. + /// + public Span Data + { + get + { + ThrowIfDisposed(); + float* p = Api.nvttSurfaceData(_ptr); + int count = Width * Height * Depth * 4; + return p == null ? Span.Empty : new Span(p, count); + } + } + + /// + /// 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. + /// + public Span Channel(int index) + { + ThrowIfDisposed(); + float* p = Api.nvttSurfaceChannel(_ptr, index); + int count = Width * Height * Depth; + return p == null ? Span.Empty : new Span(p, count); + } + + /// Populates a histogram array for the given channel. + public void Histogram(int channel, float rangeMin, float rangeMax, Span bins, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + fixed (int* b = bins) + { + Api.nvttSurfaceHistogram(_ptr, channel, rangeMin, rangeMax, bins.Length, b, tc); + } + } + + /// Returns the minimum and maximum values of a channel. + 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 + // ------------------------------------------------------------------------- + + /// Loads an image from disk. Returns false on failure. + public bool Load(string fileName, out bool hasAlpha, bool expectSigned = false, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + Span 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; + } + } + + /// Loads an image from a managed byte array. Returns false on failure. + public bool LoadFromMemory(ReadOnlySpan 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; + } + } + + /// Saves the surface to disk. Returns false on failure. + public bool Save(string fileName, bool hasAlpha = false, bool hdr = false, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + Span 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 + // ------------------------------------------------------------------------- + + /// Allocates an empty surface of the given dimensions. + public bool SetImage(int w, int h, int d = 1, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + return NvttInterop.ToBool(Api.nvttSurfaceSetImage(_ptr, w, h, d, tc)); + } + + /// Sets the surface from interleaved RGBA data. + public bool SetImageData(NvttInputFormat format, int w, int h, int d, + ReadOnlySpan 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)); + } + } + + /// Sets the surface from separate RGBA channel planes. + public bool SetImageRGBA(NvttInputFormat format, int w, int h, int d, + ReadOnlySpan r, ReadOnlySpan g, + ReadOnlySpan b, ReadOnlySpan 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)); + } + } + + /// Sets the surface from a compressed 2-D image. + public bool SetImage2D(NvttFormat format, int w, int h, + ReadOnlySpan data, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + fixed (byte* p = data) + { + return NvttInterop.ToBool( + Api.nvttSurfaceSetImage2D(_ptr, format, w, h, p, tc)); + } + } + + /// Sets the surface from a compressed 3-D image. + public bool SetImage3D(NvttFormat format, int w, int h, int d, + ReadOnlySpan data, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + fixed (byte* p = data) + { + return NvttInterop.ToBool( + Api.nvttSurfaceSetImage3D(_ptr, format, w, h, d, p, tc)); + } + } + + // ------------------------------------------------------------------------- + // Resize / mipmap + // ------------------------------------------------------------------------- + + /// Resizes the surface to the exact dimensions given. + 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); + } + + /// Resizes so that the longest extent is at most . + public void ResizeMax(int maxExtent, NvttRoundMode mode, NvttResizeFilter filter, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + Api.nvttSurfaceResizeMax(_ptr, maxExtent, mode, filter, tc); + } + + /// Resizes to a square texture with side at most . + public void ResizeMakeSquare(int maxExtent, NvttRoundMode mode, NvttResizeFilter filter, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + Api.nvttSurfaceResizeMakeSquare(_ptr, maxExtent, mode, filter, tc); + } + + /// Pads or crops the canvas to the given dimensions without resampling. + public void CanvasSize(int w, int h, int d, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + Api.nvttSurfaceCanvasSize(_ptr, w, h, d, tc); + } + + /// Returns true if a next mip level can still be generated. + public bool CanMakeNextMipmap(int minSize = 1) + { + ThrowIfDisposed(); + return NvttInterop.ToBool(Api.nvttSurfaceCanMakeNextMipmap(_ptr, minSize)); + } + + /// Generates the next mip level in place (downsamples by 2). + public bool BuildNextMipmap(NvttMipmapFilter filter, int minSize = 1, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + return NvttInterop.ToBool( + Api.nvttSurfaceBuildNextMipmapDefaults(_ptr, filter, minSize, tc)); + } + + /// Generates the next mip level using a solid colour. + 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 + // ------------------------------------------------------------------------- + + /// Converts from sRGB to linear (per-channel). + public void ToLinearFromSrgb(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToLinearFromSrgb(_ptr, tc); + } + + /// Converts from linear to sRGB (clamped). + public void ToSrgb(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToSrgb(_ptr, tc); + } + + /// Converts from sRGB to linear (unclamped). + public void ToLinearFromSrgbUnclamped(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToLinearFromSrgbUnclamped(_ptr, tc); + } + + /// Converts from linear to sRGB (unclamped). + public void ToSrgbUnclamped(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToSrgbUnclamped(_ptr, tc); + } + + /// Applies gamma expansion (toLinear) to all channels. + public void ToLinear(float gamma, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToLinear(_ptr, gamma, tc); + } + + /// Applies gamma compression to all channels. + public void ToGamma(float gamma, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToGamma(_ptr, gamma, tc); + } + + /// Applies gamma expansion to a single channel. + public void ToLinearChannel(int channel, float gamma, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToLinearChannel(_ptr, channel, gamma, tc); + } + + /// Applies gamma compression to a single channel. + public void ToGammaChannel(int channel, float gamma, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToGammaChannel(_ptr, channel, gamma, tc); + } + + // ------------------------------------------------------------------------- + // Pixel operations + // ------------------------------------------------------------------------- + + /// Applies a 4×4 colour transform matrix plus per-channel offset. + 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); + } + } + + /// Rearranges channels: result[0]=src[r], result[1]=src[g], etc. + public void Swizzle(int r, int g, int b, int a, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceSwizzle(_ptr, r, g, b, a, tc); + } + + /// Applies x = x * scale + bias to a single channel. + public void ScaleBias(int channel, float scale, float bias, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceScaleBias(_ptr, channel, scale, bias, tc); + } + + /// Clamps a channel to [, ]. + public void Clamp(int channel, float low, float high, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceClamp(_ptr, channel, low, high, tc); + } + + /// Blends toward a constant RGBA colour by factor . + 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); + } + + /// Multiplies RGB by alpha. + public void PremultiplyAlpha(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfacePremultiplyAlpha(_ptr, tc); + } + + /// Divides RGB by alpha (with epsilon guard against divide-by-zero). + public void DemultiplyAlpha(float epsilon = 1e-6f, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceDemultiplyAlpha(_ptr, epsilon, tc); + } + + /// Converts to greyscale by weighted sum of channels. + public void ToGreyScale(float redScale, float greenScale, float blueScale, + float alphaScale, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + Api.nvttSurfaceToGreyScale(_ptr, redScale, greenScale, blueScale, alphaScale, tc); + } + + /// Fills the edge border of the surface with the given colour. + public void SetBorder(float r, float g, float b, float a, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceSetBorder(_ptr, r, g, b, a, tc); + } + + /// Fills the entire surface with a constant colour. + public void Fill(float r, float g, float b, float a, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceFill(_ptr, r, g, b, a, tc); + } + + /// Scales alpha so that alpha-test coverage matches the given target. + 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 + // ------------------------------------------------------------------------- + + /// Encodes to RGBM (RGB * M, M in alpha). + public void ToRGBM(float range = 6f, float threshold = 0.25f, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToRGBM(_ptr, range, threshold, tc); + } + + /// Decodes from RGBM back to linear HDR. + public void FromRGBM(float range = 6f, float threshold = 0.25f, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceFromRGBM(_ptr, range, threshold, tc); + } + + /// Encodes to RGBE (Radiance HDR format). + public void ToRGBE(int mantissaBits = 9, int exponentBits = 5, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToRGBE(_ptr, mantissaBits, exponentBits, tc); + } + + /// Decodes from RGBE back to linear HDR. + public void FromRGBE(int mantissaBits = 9, int exponentBits = 5, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceFromRGBE(_ptr, mantissaBits, exponentBits, tc); + } + + /// Converts to YCoCg colour space. + public void ToYCoCg(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToYCoCg(_ptr, tc); + } + + /// Converts from YCoCg back to RGB. + public void FromYCoCg(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceFromYCoCg(_ptr, tc); + } + + // ------------------------------------------------------------------------- + // Normal-map operations + // ------------------------------------------------------------------------- + + /// + /// Generates a normal map from the surface (treated as a height map) + /// using four blur kernel sizes. + /// + public void ToNormalMap(float sm, float medium, float big, float large, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + Api.nvttSurfaceToNormalMap(_ptr, sm, medium, big, large, tc); + } + + /// Re-normalises all normal vectors in the surface. + public void NormalizeNormalMap(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceNormalizeNormalMap(_ptr, tc); + } + + /// Applies a normal-space transform. + public void TransformNormals(NvttNormalTransform xform, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceTransformNormals(_ptr, xform, tc); + } + + /// Reconstructs normals from a packed representation. + public void ReconstructNormals(NvttNormalTransform xform, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceReconstructNormals(_ptr, xform, tc); + } + + /// Packs normals into [0,1] range using n*scale+bias. + public void PackNormals(float scale = 0.5f, float bias = 0.5f, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfacePackNormals(_ptr, scale, bias, tc); + } + + /// Expands packed normals back to [-1,1] range. + public void ExpandNormals(float scale = 2f, float bias = -1f, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceExpandNormals(_ptr, scale, bias, tc); + } + + /// + /// Creates a Toksvig specular power map from a normal map. + /// Caller owns the returned surface. + /// + public NvttSurfaceHandle CreateToksvigMap(float power, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + return new NvttSurfaceHandle(Api.nvttSurfaceCreateToksvigMap(_ptr, power, tc)); + } + + // ------------------------------------------------------------------------- + // Flip + // ------------------------------------------------------------------------- + + /// Flips the surface along the X axis. + public void FlipX(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceFlipX(_ptr, tc); + } + + /// Flips the surface along the Y axis. + public void FlipY(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceFlipY(_ptr, tc); + } + + /// Flips the surface along the Z axis. + public void FlipZ(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceFlipZ(_ptr, tc); + } + + // ------------------------------------------------------------------------- + // Channel copy / add + // ------------------------------------------------------------------------- + + /// Copies a single channel from another surface. + public bool CopyChannel(NvttSurfaceHandle src, int srcChannel, int dstChannel, + NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + return NvttInterop.ToBool( + Api.nvttSurfaceCopyChannel(_ptr, src.Ptr, srcChannel, dstChannel, tc)); + } + + /// Adds a scaled channel from another surface into this one. + 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)); + } + + /// Copies a rectangular region from another surface into this one. + 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 + // ------------------------------------------------------------------------- + + /// Uploads the surface to the GPU (CUDA). clones instead of moving. + public void ToGPU(bool performCopy = false, NvttTimingContext* tc = null) + { + ThrowIfDisposed(); + Api.nvttSurfaceToGPU(_ptr, NvttInterop.ToNvtt(performCopy), tc); + } + + /// Downloads the surface back to CPU memory. + public void ToCPU(NvttTimingContext* tc = null) + { + ThrowIfDisposed(); Api.nvttSurfaceToCPU(_ptr, tc); + } + + // ------------------------------------------------------------------------- + // Quantize / binarize + // ------------------------------------------------------------------------- + + /// Quantizes a channel to the given bit depth. + 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); + } + + /// Binarizes a channel using a threshold. + 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)); + } } } diff --git a/src/ThridParty/Ghost.Nvtt/NvttSurfaceSet.cs b/src/ThridParty/Ghost.Nvtt/NvttSurfaceSet.cs index e72b4c5..97bd050 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttSurfaceSet.cs +++ b/src/ThridParty/Ghost.Nvtt/NvttSurfaceSet.cs @@ -1,6 +1,144 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt; + +/// +/// Wrapper around an nvtt surface set — a collection of faces and mip levels +/// loaded from a DDS file or built programmatically. +/// +public sealed unsafe class NvttSurfaceSetHandle : IDisposable { - public partial struct NvttSurfaceSet + private NvttSurfaceSet* _ptr; + + /// Raw pointer – use only when calling the native API directly. + 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 + // ------------------------------------------------------------------------- + + /// Texture dimensionality stored in this set. + public NvttTextureType TextureType + { + get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetTextureType(_ptr); } + } + + /// Number of faces (1 for 2-D / 3-D, 6 for cube maps). + public int FaceCount + { + get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetFaceCount(_ptr); } + } + + /// Number of mip levels. + public int MipmapCount + { + get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetMipmapCount(_ptr); } + } + + /// Width of the base (mip 0) image in pixels. + public int Width + { + get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetWidth(_ptr); } + } + + /// Height of the base (mip 0) image in pixels. + public int Height + { + get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetHeight(_ptr); } + } + + /// Depth of the base (mip 0) image (1 for 2-D textures). + public int Depth + { + get { ThrowIfDisposed(); return Api.nvttSurfaceSetGetDepth(_ptr); } + } + + // ------------------------------------------------------------------------- + // Surface access + // ------------------------------------------------------------------------- + + /// + /// Returns the raw pointer for the given face + /// and mip level. The pointer is owned by this surface set – do NOT dispose + /// it. + /// + public NvttSurface* GetSurfacePtr(int faceId, int mipId, bool expectSigned = false) + { + ThrowIfDisposed(); + return Api.nvttSurfaceSetGetSurface(_ptr, faceId, mipId, + NvttInterop.ToNvtt(expectSigned)); + } + + // ------------------------------------------------------------------------- + // Load / Save + // ------------------------------------------------------------------------- + + /// Resets the surface set to an empty state. + public void Reset() + { + ThrowIfDisposed(); + Api.nvttResetSurfaceSet(_ptr); + } + + /// Loads from a DDS file. Returns false on failure. + public bool LoadDDS(string fileName, bool forceNormal = false) + { + ThrowIfDisposed(); + Span 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))); + } + } + + /// Loads from a managed byte array containing DDS data. Returns false on failure. + public bool LoadDDSFromMemory(ReadOnlySpan data, bool forceNormal = false) + { + ThrowIfDisposed(); + fixed (byte* p = data) + { + return NvttInterop.ToBool( + Api.nvttSurfaceSetLoadDDSFromMemory(_ptr, p, (ulong)data.Length, + NvttInterop.ToNvtt(forceNormal))); + } + } + + /// Saves a single face/mip as an image file. Returns false on failure. + public bool SaveImage(string fileName, int faceId, int mipId) + { + ThrowIfDisposed(); + Span 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)); + } } } diff --git a/src/ThridParty/Ghost.Nvtt/NvttTimingContext.cs b/src/ThridParty/Ghost.Nvtt/NvttTimingContext.cs index bceb7c7..cc06f85 100644 --- a/src/ThridParty/Ghost.Nvtt/NvttTimingContext.cs +++ b/src/ThridParty/Ghost.Nvtt/NvttTimingContext.cs @@ -1,6 +1,99 @@ -namespace Ghost.Nvtt +namespace Ghost.Nvtt; + +/// +/// Wraps an nvtt timing context that records per-operation wall-clock times. +/// Obtain one from or create a +/// standalone instance and pass it as the optional tc parameter on +/// surface methods. +/// +public sealed unsafe class NvttTimingContextHandle : IDisposable { - public partial struct NvttTimingContext + private NvttTimingContext* _ptr; + + /// Raw pointer – use only when calling the native API directly. + public NvttTimingContext* Ptr => _ptr; + + // ------------------------------------------------------------------------- + // Construction / destruction + // ------------------------------------------------------------------------- + + /// Creates a timing context at the specified detail level (0 = off, higher = more detail). + public NvttTimingContextHandle(int detailLevel = 1) + => _ptr = Api.nvttCreateTimingContext(detailLevel); + + /// Wraps an already-owned native pointer (ownership transferred to this object). + internal NvttTimingContextHandle(NvttTimingContext* owned) => _ptr = owned; + + public void Dispose() { + if (_ptr != null) + { + Api.nvttDestroyTimingContext(_ptr); + _ptr = null; + } + } + + // ------------------------------------------------------------------------- + // Properties + // ------------------------------------------------------------------------- + + /// + /// Sets the detail level (0 = disabled; higher values record more sub-operations). + /// + public int DetailLevel + { + set + { + ThrowIfDisposed(); + Api.nvttTimingContextSetDetailLevel(_ptr, value); + } + } + + /// Number of timing records captured so far. + public int RecordCount + { + get + { + ThrowIfDisposed(); + return Api.nvttTimingContextGetRecordCount(_ptr); + } + } + + // ------------------------------------------------------------------------- + // Methods + // ------------------------------------------------------------------------- + + /// + /// Returns the description and elapsed seconds for the record at + /// . + /// + public (string description, double seconds) GetRecord(int index) + { + ThrowIfDisposed(); + Span 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); + } + } + + /// Prints all timing records to stdout via the native library. + public void PrintRecords() + { + ThrowIfDisposed(); + Api.nvttTimingContextPrintRecords(_ptr); + } + + // ------------------------------------------------------------------------- + + private void ThrowIfDisposed() + { + if (_ptr == null) + { + throw new ObjectDisposedException(nameof(NvttTimingContextHandle)); + } } } diff --git a/src/ThridParty/Ghost.Nvtt/runtime/win-x64/native/nvtt.dll b/src/ThridParty/Ghost.Nvtt/runtimes/win-x64/native/nvtt.dll similarity index 100% rename from src/ThridParty/Ghost.Nvtt/runtime/win-x64/native/nvtt.dll rename to src/ThridParty/Ghost.Nvtt/runtimes/win-x64/native/nvtt.dll