Add Ghost.Nvtt C# wrapper and integrate nvtt texture pipeline
- Introduce full managed C# wrapper for NVIDIA Texture Tools (nvtt) with safe handle classes, idiomatic APIs, and managed callback support. - Integrate Ghost.Nvtt into Ghost.Editor.Core and Ghost.MicroTest; update TextureAssetHandler to use the new nvtt wrapper for texture compression. - Add comprehensive end-to-end binding test (NvttBindingTest). - Refactor D3D12 resource management: add deferred/immediate release APIs, update allocator/database usage, and ensure proper resource cleanup. - Update project files for new native DLL layout and dependency versions. - Minor API cleanups: EditorApplication properties, D3D12 input layout, and removal of obsolete code. - Update shaders, tests, and documentation for new APIs and usage patterns.
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
@@ -178,8 +176,4 @@ public readonly struct AssetReference : IEquatable<AssetReference>
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAssetSettings
|
||||
{
|
||||
ValueTask<Result<long>> WriteToStreamAsync(Stream stream, CancellationToken token = default);
|
||||
ValueTask<Result<IAssetSettings>> ReadFromStreamAsync(Stream stream, CancellationToken token = default);
|
||||
}
|
||||
public interface IAssetSettings;
|
||||
|
||||
@@ -11,29 +11,23 @@ public sealed class CustomAssetHandlerAttribute : Attribute
|
||||
get; init;
|
||||
}
|
||||
|
||||
public bool AllowCaching
|
||||
{
|
||||
get; init;
|
||||
} = true;
|
||||
|
||||
public required string[] SupportedExtensions
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DependencyUpdateType
|
||||
{
|
||||
Add,
|
||||
Remove
|
||||
public bool AllowCaching
|
||||
{
|
||||
get; init;
|
||||
} = true;
|
||||
}
|
||||
|
||||
public interface IAssetExportOptions;
|
||||
|
||||
public interface IAssetHandler
|
||||
{
|
||||
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default);
|
||||
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default);
|
||||
}
|
||||
|
||||
public interface IImportableAssetHandler : IAssetHandler
|
||||
|
||||
37
src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs
Normal file
37
src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class CustomAssetProcesserAttribute<T> : Attribute
|
||||
{
|
||||
public Type Type => typeof(T);
|
||||
}
|
||||
|
||||
public readonly struct AssetProcesserContext
|
||||
{
|
||||
public IAssetRegistry Registry
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public string AssetPath
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public Asset Asset
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public IAssetHandler Handler
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAssetProcesser
|
||||
{
|
||||
ValueTask ProcessAsync(AssetProcesserContext ctx);
|
||||
}
|
||||
@@ -41,13 +41,6 @@ public enum TextureCompressionLevel : uint
|
||||
High
|
||||
}
|
||||
|
||||
public enum TextureCompressionEffort : uint
|
||||
{
|
||||
Fastest,
|
||||
Normal,
|
||||
Production
|
||||
}
|
||||
|
||||
public enum MipmapFilter : uint
|
||||
{
|
||||
Box,
|
||||
@@ -59,14 +52,17 @@ public enum MipmapFilter : uint
|
||||
public class TextureAsset : Asset
|
||||
{
|
||||
internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F";
|
||||
|
||||
internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID);
|
||||
|
||||
public override Guid TypeID => s_typeGuid;
|
||||
private readonly Handle<Texture> _texture;
|
||||
|
||||
public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings)
|
||||
public override Guid TypeID => s_typeGuid;
|
||||
public Handle<Texture> Texture => _texture;
|
||||
|
||||
public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings, Handle<Texture> texture)
|
||||
: base(id, dependencies, settings)
|
||||
{
|
||||
_texture = texture;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,21 +138,16 @@ public class TextureAssetSettings : IAssetSettings
|
||||
get; set;
|
||||
} = TextureCompressionLevel.Normal;
|
||||
|
||||
public TextureCompressionEffort CompressionEffort
|
||||
{
|
||||
get; set;
|
||||
} = TextureCompressionEffort.Normal;
|
||||
|
||||
public bool UseBorderColor
|
||||
{
|
||||
get; set;
|
||||
} = false;
|
||||
|
||||
public Color32 BorderColor
|
||||
public Color128 BorderColor
|
||||
{
|
||||
get; set;
|
||||
} = new Color32(0, 0, 0, 0);
|
||||
|
||||
} = new Color128(0, 0, 0, 0);
|
||||
|
||||
public bool ZeroAlphaBorder
|
||||
{
|
||||
get; set;
|
||||
@@ -254,11 +245,12 @@ public class TextureAssetSettings : IAssetSettings
|
||||
|
||||
try
|
||||
{
|
||||
ref byte address = ref MemoryMarshal.GetReference(tempArray);
|
||||
await stream.ReadAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false);
|
||||
var basic = Unsafe.ReadUnaligned<BasicSettings>(ref address);
|
||||
var advanced = Unsafe.ReadUnaligned<AdvancedSettings>(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>()));
|
||||
var sampler = Unsafe.ReadUnaligned<SamplerSettings>(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()));
|
||||
|
||||
// Use index-based reads after the await to avoid 'ref across await' errors.
|
||||
var basic = Unsafe.ReadUnaligned<BasicSettings>(ref tempArray[0]);
|
||||
var advanced = Unsafe.ReadUnaligned<AdvancedSettings>(ref tempArray[Unsafe.SizeOf<BasicSettings>()]);
|
||||
var sampler = Unsafe.ReadUnaligned<SamplerSettings>(ref tempArray[Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()]);
|
||||
|
||||
var settings = new TextureAssetSettings
|
||||
{
|
||||
@@ -280,6 +272,7 @@ public class TextureAssetSettings : IAssetSettings
|
||||
}
|
||||
}
|
||||
|
||||
[CustomAssetHandler(ID = TextureAsset._TYPE_ID, SupportedExtensions = new[] { ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" })]
|
||||
internal class TextureAssetHandler : IImportableAssetHandler
|
||||
{
|
||||
private const int _CURRENT_VERSION = 1;
|
||||
@@ -291,87 +284,112 @@ internal class TextureAssetHandler : IImportableAssetHandler
|
||||
|
||||
public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default)
|
||||
{
|
||||
// ---- 1. Probe image info -----------------------------------------------
|
||||
var info = ImageInfo.FromStream(sourceStream);
|
||||
if (info.BitsPerChannel <= 0)
|
||||
{
|
||||
return Result.Failure($"Unsupported image format with {info.BitsPerChannel} bits per channel.");
|
||||
}
|
||||
|
||||
ref byte pData = ref Unsafe.NullRef<byte>();
|
||||
var imageSize = 0ul;
|
||||
var isFloat = info.BitsPerChannel > 8;
|
||||
var width = info.Width;
|
||||
var height = info.Height;
|
||||
var colorComponents = info.ColorComponents;
|
||||
|
||||
// ---- 2. Decode pixels into a managed byte[] ----------------------------
|
||||
byte[] pixelBytes;
|
||||
|
||||
if (isFloat)
|
||||
{
|
||||
using var image = ImageResultFloat.FromStream(sourceStream, info.ColorComponents);
|
||||
pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan()));
|
||||
imageSize = image.Size;
|
||||
using var image = ImageResultFloat.FromStream(sourceStream, colorComponents);
|
||||
var span = MemoryMarshal.AsBytes(image.AsSpan());
|
||||
pixelBytes = new byte[span.Length];
|
||||
span.CopyTo(pixelBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var image = ImageResult.FromStream(sourceStream, info.ColorComponents);
|
||||
pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan()));
|
||||
imageSize = image.Size;
|
||||
using var image = ImageResult.FromStream(sourceStream, colorComponents);
|
||||
var span = MemoryMarshal.AsBytes(image.AsSpan());
|
||||
pixelBytes = new byte[span.Length];
|
||||
span.CopyTo(pixelBytes);
|
||||
}
|
||||
|
||||
// ---- 3. Run NVTT compression on a thread-pool thread (side-effect only) -
|
||||
// The cache path is derivable at any time from (id, settingsHash), so we
|
||||
// do NOT store it in the asset file. LoadAsync/SaveAsync will recompute it.
|
||||
var settings = new TextureAssetSettings();
|
||||
await Task.Run(() =>
|
||||
TextureProcessor.CompressToCache(
|
||||
EditorApplication.CachesFolderPath,
|
||||
id,
|
||||
pixelBytes,
|
||||
width,
|
||||
height,
|
||||
isFloat,
|
||||
colorComponents,
|
||||
settings),
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
// ---- 4. Write asset file: header + settings + raw image data -----------
|
||||
// Content layout (all little-endian):
|
||||
// int32 width
|
||||
// int32 height
|
||||
// byte isFloat (0 = byte, 1 = float)
|
||||
// int32 colorComponents (cast of ColorComponents enum)
|
||||
// byte[] pixelBytes
|
||||
const int _CONTENT_HEADER_SIZE = 4 + 4 + 1 + 4; // 13 bytes
|
||||
var contentSize = _CONTENT_HEADER_SIZE + pixelBytes.Length;
|
||||
|
||||
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
|
||||
{
|
||||
HandlerVersion = _CURRENT_VERSION,
|
||||
SettingsOffset = AssetMetadata.SIZE,
|
||||
};
|
||||
|
||||
// Reserve space for the header, then write settings
|
||||
targetStream.Seek(0, SeekOrigin.Begin);
|
||||
AssetMetadata.WriteToStream(targetStream, ref header);
|
||||
|
||||
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
|
||||
var settings = new TextureAssetSettings();
|
||||
var sizeResult = await settings.WriteToStreamAsync(targetStream, token).ConfigureAwait(false);
|
||||
if (sizeResult.IsFailure)
|
||||
{
|
||||
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
|
||||
}
|
||||
|
||||
header.SettingsSize = sizeResult.Value;
|
||||
header.SettingsSize = sizeResult.Value;
|
||||
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
|
||||
header.ContentSize = (long)imageSize;
|
||||
header.ContentSize = contentSize;
|
||||
|
||||
// Write raw image content
|
||||
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
|
||||
|
||||
var offset = 0;
|
||||
var tempArray = ArrayPool<byte>.Shared.Rent((int)Math.Min(imageSize, 40960ul));
|
||||
var remaining = imageSize;
|
||||
|
||||
var contentHeader = ArrayPool<byte>.Shared.Rent(_CONTENT_HEADER_SIZE);
|
||||
try
|
||||
{
|
||||
while (remaining > 0)
|
||||
{
|
||||
var chunkSize = (int)Math.Min(remaining, (ulong)tempArray.Length);
|
||||
Unsafe.CopyBlockUnaligned(ref tempArray[0], ref Unsafe.Add(ref pData, offset), (uint)chunkSize);
|
||||
|
||||
await targetStream.WriteAsync(tempArray.AsMemory(0, chunkSize), token).ConfigureAwait(false);
|
||||
|
||||
offset += chunkSize;
|
||||
remaining -= (ulong)chunkSize;
|
||||
}
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to write texture asset content to stream: {ex.Message}");
|
||||
BitConverter.TryWriteBytes(contentHeader.AsSpan(0, 4), width);
|
||||
BitConverter.TryWriteBytes(contentHeader.AsSpan(4, 4), height);
|
||||
contentHeader[8] = isFloat ? (byte)1 : (byte)0;
|
||||
BitConverter.TryWriteBytes(contentHeader.AsSpan(9, 4), (int)colorComponents);
|
||||
await targetStream.WriteAsync(contentHeader.AsMemory(0, _CONTENT_HEADER_SIZE), token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(tempArray);
|
||||
ArrayPool<byte>.Shared.Return(contentHeader);
|
||||
}
|
||||
|
||||
await targetStream.WriteAsync(pixelBytes, token).ConfigureAwait(false);
|
||||
await targetStream.FlushAsync(token).ConfigureAwait(false);
|
||||
|
||||
// Patch header now that all sizes are known
|
||||
targetStream.Seek(0, SeekOrigin.Begin);
|
||||
AssetMetadata.WriteToStream(targetStream, ref header);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default)
|
||||
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default)
|
||||
public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
259
src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs
Normal file
259
src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using Ghost.Nvtt;
|
||||
using Ghost.Nvtt.Native;
|
||||
using Misaki.HighPerformance.Image;
|
||||
using System.IO.Hashing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Drives the NVTT compression + mipmap pipeline for a single texture asset.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// 1. Accept raw decoded pixel bytes + settings.
|
||||
/// 2. Determine the cache file path (<c>CachesFolderPath/TextureCache/<guid>_<hash>.dds</c>).
|
||||
/// 3. If the cache is already valid (hash matches), skip compression.
|
||||
/// 4. Otherwise run the full NVTT pipeline and write the DDS to the cache file.
|
||||
///
|
||||
/// The caller owns opening/closing all streams; this class only takes spans and paths.
|
||||
/// </summary>
|
||||
internal static unsafe class TextureProcessor
|
||||
{
|
||||
private const string _TEXTURE_CACHE_SUBFOLDER = "TextureCache";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public entry point
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Compresses <paramref name="pixelData"/> according to <paramref name="settings"/>
|
||||
/// and writes the result to the texture cache.
|
||||
///
|
||||
/// Returns the absolute path of the cache file on success.
|
||||
/// The cache file is skipped if it already exists with a matching content hash.
|
||||
/// </summary>
|
||||
public static string CompressToCache(
|
||||
string cachesFolderPath,
|
||||
Guid assetId,
|
||||
ReadOnlySpan<byte> pixelData,
|
||||
int width,
|
||||
int height,
|
||||
bool isFloat,
|
||||
ColorComponents colorComponents,
|
||||
TextureAssetSettings settings)
|
||||
{
|
||||
// --- derive cache path --------------------------------------------------
|
||||
var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER);
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
|
||||
var settingsHash = ComputeSettingsHash(settings);
|
||||
var cacheFileName = $"{assetId:N}_{settingsHash:X16}.dds";
|
||||
var cachePath = Path.Combine(cacheDir, cacheFileName);
|
||||
|
||||
// --- check validity: same file name = same settings hash = already done -
|
||||
if (File.Exists(cachePath))
|
||||
{
|
||||
return cachePath;
|
||||
}
|
||||
|
||||
// --- delete any stale cache entries for this asset ----------------------
|
||||
foreach (var stale in Directory.EnumerateFiles(cacheDir, $"{assetId:N}_*.dds"))
|
||||
{
|
||||
File.Delete(stale);
|
||||
}
|
||||
|
||||
// --- run NVTT pipeline --------------------------------------------------
|
||||
RunNvttPipeline(cachePath, pixelData, width, height, isFloat, colorComponents, settings);
|
||||
|
||||
return cachePath;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// NVTT pipeline
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static void RunNvttPipeline(
|
||||
string outputPath,
|
||||
ReadOnlySpan<byte> pixelData,
|
||||
int width,
|
||||
int height,
|
||||
bool isFloat,
|
||||
ColorComponents colorComponents,
|
||||
TextureAssetSettings settings)
|
||||
{
|
||||
using var surface = new NvttSurfaceHandle();
|
||||
using var compOpts = new NvttCompressionOptionsHandle();
|
||||
using var outOpts = new NvttOutputOptionsHandle();
|
||||
using var ctx = new NvttContextHandle();
|
||||
|
||||
// ---- 1. load pixels into NVTT -----------------------------------------
|
||||
// Misaki.HighPerformance.Image always decodes to RGBA channel order.
|
||||
// Float images → RGBA_32F, byte images → BGRA_8UB.
|
||||
// NOTE: NVTT BGRA_8UB expects Blue in byte[0]; stb decodes RGBA so we need
|
||||
// to pass RGBA. There is no RGBA_8UB enum — we swizzle after load instead.
|
||||
var inputFormat = isFloat
|
||||
? NvttInputFormat.NVTT_InputFormat_RGBA_32F
|
||||
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
|
||||
|
||||
surface.SetImageData(inputFormat, width, height, 1, pixelData);
|
||||
|
||||
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA,
|
||||
// so channels R and B are swapped — fix with swizzle(2,1,0,3).
|
||||
if (!isFloat)
|
||||
{
|
||||
surface.Swizzle(2, 1, 0, 3);
|
||||
}
|
||||
|
||||
// ---- 2. resize ---------------------------------------------------------
|
||||
var maxExtent = (int)settings.Sampler.MaxSize;
|
||||
if (settings.Advanced.StretchToPowerOfTwo)
|
||||
{
|
||||
surface.ResizeMakeSquare(maxExtent,
|
||||
NvttRoundMode.NVTT_RoundMode_ToPreviousPowerOfTwo,
|
||||
NvttResizeFilter.NVTT_ResizeFilter_Box);
|
||||
}
|
||||
else if (surface.Width > maxExtent || surface.Height > maxExtent)
|
||||
{
|
||||
surface.ResizeMax(maxExtent,
|
||||
NvttRoundMode.NVTT_RoundMode_None,
|
||||
NvttResizeFilter.NVTT_ResizeFilter_Box);
|
||||
}
|
||||
|
||||
// ---- 2b. border color --------------------------------------------------
|
||||
if (settings.Advanced.UseBorderColor)
|
||||
{
|
||||
var c = settings.Advanced.BorderColor;
|
||||
surface.SetBorder(c.r, c.g, c.b, c.a);
|
||||
}
|
||||
else if (settings.Advanced.ZeroAlphaBorder)
|
||||
{
|
||||
surface.SetBorder(0f, 0f, 0f, 0f);
|
||||
}
|
||||
|
||||
// ---- 3. colour-space: convert to linear before mip filtering -----------
|
||||
if (settings.Basic.IsSRGB && settings.Advanced.GammaCorrection)
|
||||
{
|
||||
surface.ToLinearFromSrgb();
|
||||
}
|
||||
|
||||
// ---- 4. premultiply alpha (before mip chain) ---------------------------
|
||||
if (settings.Advanced.PremultiplyAlpha)
|
||||
{
|
||||
surface.PremultiplyAlpha();
|
||||
}
|
||||
|
||||
// ---- 5. configure compression options ----------------------------------
|
||||
compOpts.Format = SelectFormat(settings);
|
||||
compOpts.Quality = SelectQuality(settings.Advanced.CompressionLevel);
|
||||
|
||||
if (settings.Advanced.CutoutAlpha)
|
||||
{
|
||||
compOpts.SetQuantization(false, false, true,
|
||||
settings.Advanced.CutoutAlphaThreshold);
|
||||
}
|
||||
|
||||
// ---- 6. configure output options ---------------------------------------
|
||||
outOpts.OutputHeader = true;
|
||||
outOpts.Srgb = settings.Basic.IsSRGB;
|
||||
outOpts.Container = NvttContainer.NVTT_Container_DDS10;
|
||||
outOpts.FileName = outputPath;
|
||||
|
||||
// ---- 7. mipmap count ---------------------------------------------------
|
||||
var nvttFilter = SelectMipmapFilter(settings.Advanced.MipmapFilter);
|
||||
|
||||
int mipmapCount;
|
||||
if (!settings.Advanced.GenerateMipmaps)
|
||||
{
|
||||
mipmapCount = 1;
|
||||
}
|
||||
else if (settings.Advanced.MipmapLevelCount == 0)
|
||||
{
|
||||
mipmapCount = surface.CountMipmaps();
|
||||
}
|
||||
else
|
||||
{
|
||||
mipmapCount = (int)settings.Advanced.MipmapLevelCount;
|
||||
}
|
||||
|
||||
// ---- 8. enable CUDA if available ---------------------------------------
|
||||
ctx.SetCudaAcceleration(Ghost.Nvtt.NvttGlobal.IsCudaSupported);
|
||||
|
||||
// ---- 9. write DDS header -----------------------------------------------
|
||||
ctx.OutputHeader(surface, mipmapCount, compOpts, outOpts);
|
||||
|
||||
// ---- 10. compress mip chain using a working clone ----------------------
|
||||
using var mip = surface.Clone();
|
||||
|
||||
for (int level = 0; level < mipmapCount; level++)
|
||||
{
|
||||
// Scale alpha for coverage on each mip (if requested)
|
||||
if (settings.Advanced.ScaleAlphaForMipCoverage && level > 0)
|
||||
{
|
||||
float refCoverage = mip.AlphaTestCoverage(
|
||||
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f);
|
||||
mip.ScaleAlphaToCoverage(refCoverage,
|
||||
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f);
|
||||
}
|
||||
|
||||
ctx.Compress(mip, face: 0, mipmap: level, compOpts, outOpts);
|
||||
|
||||
if (level + 1 < mipmapCount)
|
||||
{
|
||||
mip.BuildNextMipmap(nvttFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static NvttFormat SelectFormat(TextureAssetSettings settings)
|
||||
=> settings.Basic.TextureType switch
|
||||
{
|
||||
TextureType.Normal => NvttFormat.NVTT_Format_BC5, // RG normal map
|
||||
TextureType.SingleChannel => NvttFormat.NVTT_Format_BC4, // single channel
|
||||
TextureType.Lightmap => NvttFormat.NVTT_Format_BC6U, // HDR lightmap (unsigned)
|
||||
_ => NvttFormat.NVTT_Format_BC7, // default colour
|
||||
};
|
||||
|
||||
private static NvttQuality SelectQuality(TextureCompressionLevel level)
|
||||
=> level switch
|
||||
{
|
||||
TextureCompressionLevel.Low => NvttQuality.NVTT_Quality_Fastest,
|
||||
TextureCompressionLevel.High => NvttQuality.NVTT_Quality_Production,
|
||||
_ => NvttQuality.NVTT_Quality_Normal,
|
||||
};
|
||||
|
||||
private static NvttMipmapFilter SelectMipmapFilter(MipmapFilter filter)
|
||||
=> filter switch
|
||||
{
|
||||
MipmapFilter.Box => NvttMipmapFilter.NVTT_MipmapFilter_Box,
|
||||
MipmapFilter.Triangle => NvttMipmapFilter.NVTT_MipmapFilter_Triangle,
|
||||
MipmapFilter.MitchellNetravali => NvttMipmapFilter.NVTT_MipmapFilter_Mitchell,
|
||||
_ => NvttMipmapFilter.NVTT_MipmapFilter_Kaiser,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Produces a stable 64-bit hash of the settings structs so the cache file
|
||||
/// name changes whenever any setting changes.
|
||||
/// </summary>
|
||||
private static ulong ComputeSettingsHash(TextureAssetSettings s)
|
||||
{
|
||||
var basicSize = Unsafe.SizeOf<TextureAssetSettings.BasicSettings>();
|
||||
var advancedSize = Unsafe.SizeOf<TextureAssetSettings.AdvancedSettings>();
|
||||
var samplerSize = Unsafe.SizeOf<TextureAssetSettings.SamplerSettings>();
|
||||
var total = basicSize + advancedSize + samplerSize;
|
||||
|
||||
Span<byte> buf = stackalloc byte[total];
|
||||
var basic = s.Basic;
|
||||
var advanced = s.Advanced;
|
||||
var sampler = s.Sampler;
|
||||
MemoryMarshal.Write(buf, in basic);
|
||||
MemoryMarshal.Write(buf.Slice(basicSize), in advanced);
|
||||
MemoryMarshal.Write(buf.Slice(basicSize + advancedSize), in sampler);
|
||||
|
||||
return XxHash64.HashToUInt64(buf);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
@@ -7,7 +6,10 @@ namespace Ghost.Editor.Core;
|
||||
public static class EditorApplication
|
||||
{
|
||||
public const string ASSETS_FOLDER_NAME = "Assets";
|
||||
public const string SOURCES_FOLDER_NAME = "Sources";
|
||||
public const string PACKAGES_FOLDER_NAME = "Packages";
|
||||
public const string CACHES_FOLDER_NAME = "Caches";
|
||||
public const string CONFIG_FOLDER_NAME = "Config";
|
||||
|
||||
private static IServiceProvider? s_serviceProvider;
|
||||
private static string s_currentProjectPath = string.Empty;
|
||||
@@ -16,8 +18,15 @@ public static class EditorApplication
|
||||
private static DispatcherQueue? s_dispatcherQueue;
|
||||
|
||||
internal static Application CurrentApplication => Application.Current;
|
||||
internal static string CurrentProjectPath => s_currentProjectPath;
|
||||
internal static string CurrentProjectName => s_currentProjectName;
|
||||
|
||||
public static string ProjectPath => s_currentProjectPath;
|
||||
public static string ProjectName => s_currentProjectName;
|
||||
|
||||
public static string AssetsFolderPath => Path.Combine(ProjectPath, ASSETS_FOLDER_NAME);
|
||||
public static string SourcesFolderPath => Path.Combine(ProjectPath, SOURCES_FOLDER_NAME);
|
||||
public static string PackagesFolderPath => Path.Combine(ProjectPath, PACKAGES_FOLDER_NAME);
|
||||
public static string CachesFolderPath => Path.Combine(ProjectPath, CACHES_FOLDER_NAME);
|
||||
public static string ConfigFolderPath => Path.Combine(ProjectPath, CONFIG_FOLDER_NAME);
|
||||
|
||||
public static DispatcherQueue DispatcherQueue
|
||||
{
|
||||
@@ -57,9 +66,5 @@ public static class EditorApplication
|
||||
|
||||
internal static void Shutdown()
|
||||
{
|
||||
if (s_serviceProvider?.GetService(typeof(IAssetService)) is AssetHandle.AssetService assetService)
|
||||
{
|
||||
assetService.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
|
||||
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -39,7 +39,7 @@ internal sealed partial class EngineEditorWindow : WindowEx
|
||||
|
||||
private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
PART_TitleBar.Title = EditorApplication.CurrentProjectName;
|
||||
PART_TitleBar.Title = EditorApplication.ProjectName;
|
||||
PART_TitleBar.Subtitle = $"Ghost Engine {Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}";
|
||||
|
||||
_notificationService.SetReference(InfoBar, NotificationQueue);
|
||||
|
||||
@@ -26,7 +26,7 @@ internal sealed partial class SplashWindow : WindowEx
|
||||
{
|
||||
var version = Package.Current.Id.Version;
|
||||
VersionTextBlock.Text = $"Version {version.Major}.{version.Minor}.{version.Build}";
|
||||
LoadingTextBlock.Text = $"Loading {EditorApplication.CurrentProjectName}...";
|
||||
LoadingTextBlock.Text = $"Loading {EditorApplication.ProjectName}...";
|
||||
CopyrightTextBlock.Text = $"Copyright © {DateTime.Now.Year} Ghost Engine. All rights reserved.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ internal partial class ProjectBrowserViewModel : ObservableObject
|
||||
_inspectorService = inspectorService;
|
||||
_assetService = assetService;
|
||||
|
||||
var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
LoadSubFolderRecursive(assetsRootItem);
|
||||
|
||||
Directories.Add(assetsRootItem);
|
||||
|
||||
@@ -41,7 +41,7 @@ internal partial class ProjectViewModel : ObservableObject
|
||||
{
|
||||
_assetService = assetService;
|
||||
|
||||
var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
LoadSubFolderRecursive(ref assetsRootItem);
|
||||
|
||||
SubDirectories.Add(assetsRootItem);
|
||||
|
||||
Reference in New Issue
Block a user