feat(rendering): add GPU scene updates and optimizations

Added a new `code-executor` agent with strict TDD and performance focus. Refactored `TextureProcessor` and `TextureAssetHandler` to use `Magick.NET` for image processing. Enhanced `GPUScene` with `InstanceCounterBuffer` and improved instance management. Introduced a compute shader for GPU scene updates. Updated `GhostRenderPipeline` to handle add/remove instance buffers.

BREAKING CHANGE: Removed `x86` platform support and replaced `CachesFolderPath` with `LibraryFolderPath`. Updated project dependencies and removed unused utility classes.
This commit is contained in:
2026-04-14 17:56:23 +09:00
parent 817b32b8d9
commit d9bfa43663
28 changed files with 517 additions and 459 deletions

View File

@@ -1,11 +1,7 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Core.Utilities;
using Ghost.DSL.ShaderParser;
using Misaki.HighPerformance.Utilities;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Ghost.DSL.ShaderCompiler;
@@ -24,17 +20,6 @@ public struct DSLShaderError
internal static class DSLShaderCompiler
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong GetUniqueId(string code)
{
if (string.IsNullOrEmpty(code))
{
return 0;
}
return XxHash64.HashToUInt64(MemoryMarshal.AsBytes(code.AsSpan()));
}
private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent)
{
if (semantic == null)
@@ -163,7 +148,7 @@ internal static class DSLShaderCompiler
passes = passes
};
for (int i = 0; i < descriptor.passes.Length; i++)
for (var i = 0; i < descriptor.passes.Length; i++)
{
descriptor.passes[i].shader = descriptor;
}
@@ -283,7 +268,7 @@ internal static class DSLShaderCompiler
}
var shaderCodes = new ShaderCode[semantics.entryPoints.Count];
for (int i = 0; i < shaderCodes.Length; i++)
for (var i = 0; i < shaderCodes.Length; i++)
{
var result = BuildFinalShaderCode(semantics.entryPoints[i].shaderPath, semantics.includes.AsSpan(), semantics.hlsl, propertyInfo.code);
if (result.IsFailure)

View File

@@ -1,7 +1,7 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Image;
using ImageMagick;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -218,6 +218,14 @@ internal class TextureAssetHandler : IImportableAssetHandler
{
private const int _CURRENT_VERSION = 1;
private struct ImageContentHeader
{
public uint width;
public uint height;
public uint depth;
public uint colorComponents;
}
private static async ValueTask<Result<long>> WriteSettingsToStreamAsync(TextureAssetSettings settings, Stream stream, CancellationToken token = default)
{
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
@@ -284,105 +292,60 @@ internal class TextureAssetHandler : IImportableAssetHandler
public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default)
{
var info = ImageInfo.FromStream(sourceStream);
if (info.BitsPerChannel <= 0)
using var image = new MagickImage(sourceStream);
var bytes = image.ToByteArray();
var settings = new TextureAssetSettings();
await TextureProcessor.CompressToCacheAsync(EditorApplication.LibraryFolderPath, id, bytes, image.Width, image.Height, image.Depth, settings, token).ConfigureAwait(false);
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{
return Result.Failure($"Unsupported image format with {info.BitsPerChannel} bits per channel.");
HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE,
};
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
var sizeResult = await WriteSettingsToStreamAsync(settings, targetStream, token).ConfigureAwait(false);
if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
}
var isFloat = info.BitsPerChannel > 8;
var width = info.Width;
var height = info.Height;
var colorComponents = info.ColorComponents;
// Content layout (all little-endian):
// uint32 width
// uint32 height
// uint32 depth
// uint32 colorComponents
// byte[] pixelBytes
byte[] pixelBytes;
if (isFloat)
header.SettingsSize = sizeResult.Value;
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
unsafe
{
using var image = ImageResultFloat.FromStream(sourceStream, colorComponents);
var span = MemoryMarshal.AsBytes(image.AsSpan());
pixelBytes = ArrayPool<byte>.Shared.Rent(span.Length);
span.CopyTo(pixelBytes);
}
else
{
using var image = ImageResult.FromStream(sourceStream, colorComponents);
var span = image.AsSpan();
pixelBytes = ArrayPool<byte>.Shared.Rent(span.Length);
span.CopyTo(pixelBytes);
header.ContentSize = sizeof(ImageContentHeader) + image.Width * image.Height * (image.Depth / 8) * image.ChannelCount;
}
try
// Write raw image content
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var contentHeader = new ImageContentHeader
{
var settings = new TextureAssetSettings();
await Task.Run(() =>
TextureProcessor.CompressToCache(
EditorApplication.CachesFolderPath,
id,
pixelBytes,
width,
height,
isFloat,
colorComponents,
settings),
token).ConfigureAwait(false);
width = image.Width,
height = image.Height,
depth = image.Depth,
colorComponents = image.ChannelCount
};
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{
HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE,
};
targetStream.Write(MemoryMarshal.AsBytes(new Span<ImageContentHeader>(ref contentHeader)));
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
var sizeResult = await WriteSettingsToStreamAsync(settings, targetStream, token).ConfigureAwait(false);
if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
}
await targetStream.WriteAsync(bytes, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
// 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
// Patch header now that all sizes are known
targetStream.Seek(0, SeekOrigin.Begin);
AssetMetadata.WriteToStream(targetStream, ref header);
header.SettingsSize = sizeResult.Value;
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
header.ContentSize = _CONTENT_HEADER_SIZE + pixelBytes.Length;
// Write raw image content
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var contentHeader = ArrayPool<byte>.Shared.Rent(_CONTENT_HEADER_SIZE);
try
{
BitConverter.TryWriteBytes(contentHeader.AsSpan(0, 4), width);
BitConverter.TryWriteBytes(contentHeader.AsSpan(4, 4), height);
contentHeader[8] = isFloat ? (byte)1 : (byte)0;
BitConverter.TryWriteBytes(contentHeader.AsSpan(9, 4), (int)colorComponents);
await targetStream.WriteAsync(contentHeader.AsMemory(0, _CONTENT_HEADER_SIZE), token).ConfigureAwait(false);
}
finally
{
ArrayPool<byte>.Shared.Return(contentHeader);
}
await targetStream.WriteAsync(pixelBytes, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
// Patch header now that all sizes are known
targetStream.Seek(0, SeekOrigin.Begin);
AssetMetadata.WriteToStream(targetStream, ref header);
return Result.Success();
}
finally
{
ArrayPool<byte>.Shared.Return(pixelBytes);
}
return Result.Success();
}
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)

View File

@@ -1,5 +1,5 @@
using Ghost.Nvtt;
using Misaki.HighPerformance.Image;
using ImageMagick;
using Misaki.HighPerformance.LowLevel;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
@@ -19,26 +19,152 @@ namespace Ghost.Editor.Core.AssetHandler;
///
/// The caller owns opening/closing all streams; this class only takes spans and paths.
/// </summary>
internal static unsafe class TextureProcessor
internal static class TextureProcessor
{
private class NvttPipelineTask : IThreadPoolWorkItem
{
private readonly string _outputPath;
private readonly byte[] _image;
private readonly uint _depth;
private readonly uint _width;
private readonly uint _height;
private readonly TextureAssetSettings _settings;
private readonly TaskCompletionSource _completionSource;
public Task Task => _completionSource.Task;
public NvttPipelineTask(string outputPath, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings)
{
_outputPath = outputPath;
_image = image;
_width = width;
_height = height;
_depth = depth;
_settings = settings;
_completionSource = new TaskCompletionSource();
}
public unsafe void Execute()
{
using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create());
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
using var pOutOpts = new DisposablePtr<NvttOutputOptions>(NvttOutputOptions.Create());
using var pCtx = new DisposablePtr<NvttContext>(NvttContext.Create());
var inputFormat = _depth > 8
? NvttInputFormat.NVTT_InputFormat_RGBA_32F
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
fixed (void* pData = _image)
{
pSurface.Get()->SetImageData(inputFormat, (int)_width, (int)_height, 1, pData, NvttBoolean.NVTT_True, null);
}
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA,
// so channels R and B are swapped — fix with swizzle(2,1,0,3).
if (_depth <= 8)
{
pSurface.Get()->Swizzle(2, 1, 0, 3, null);
}
var maxExtent = (int)_settings.Sampler.MaxSize;
if (_settings.Advanced.StretchToPowerOfTwo)
{
pSurface.Get()->ResizeMakeSquare(maxExtent,
NvttRoundMode.NVTT_RoundMode_ToNearestPowerOfTwo,
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
}
else if (pSurface.Get()->Width() > maxExtent || pSurface.Get()->Height() > maxExtent)
{
pSurface.Get()->ResizeMax(maxExtent,
NvttRoundMode.NVTT_RoundMode_None,
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
}
if (_settings.Advanced.UseBorderColor)
{
var c = _settings.Advanced.BorderColor;
pSurface.Get()->SetBorder(c.r, c.g, c.b, c.a, null);
}
else if (_settings.Advanced.ZeroAlphaBorder)
{
pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null);
}
if (_settings.Basic.IsSRGB && _settings.Advanced.GammaCorrection)
{
pSurface.Get()->ToLinearFromSrgb(null);
}
if (_settings.Advanced.PremultiplyAlpha)
{
pSurface.Get()->PremultiplyAlpha(null);
}
pCompOpts.Get()->SetFormat(SelectFormat(_settings));
pCompOpts.Get()->SetQuality(SelectQuality(_settings.Advanced.CompressionLevel));
if (_settings.Advanced.CutoutAlpha)
{
pCompOpts.Get()->SetQuantization(false, false, true,
_settings.Advanced.CutoutAlphaThreshold);
}
pOutOpts.Get()->SetOutputHeader(true);
pOutOpts.Get()->SetSrgbFlag(_settings.Basic.IsSRGB);
pOutOpts.Get()->SetContainer(NvttContainer.NVTT_Container_DDS10);
pOutOpts.Get()->SetFileName(Encoding.UTF8.GetBytes(_outputPath));
var nvttFilter = SelectMipmapFilter(_settings.Advanced.MipmapFilter);
int mipmapCount;
if (!_settings.Advanced.GenerateMipmaps)
{
mipmapCount = 1;
}
else if (_settings.Advanced.MipmapLevelCount == 0)
{
mipmapCount = pSurface.Get()->CountMipmaps(1);
}
else
{
mipmapCount = (int)_settings.Advanced.MipmapLevelCount;
}
pCtx.Get()->SetCudaAcceleration(NvttApi.IsCudaSupported());
pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get());
using var pMip = new DisposablePtr<NvttSurface>(pSurface.Get()->Clone());
for (var level = 0; level < mipmapCount; level++)
{
// Scale alpha for coverage on each pMip (if requested)
if (_settings.Advanced.ScaleAlphaForMipCoverage && level > 0)
{
var refCoverage = pMip.Get()->AlphaTestCoverage(
_settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3);
pMip.Get()->ScaleAlphaToCoverage(refCoverage,
_settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null);
}
pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get());
if (level + 1 < mipmapCount)
{
pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null);
}
}
_completionSource.SetResult();
}
}
private const string _TEXTURE_CACHE_SUBFOLDER = "TextureCache";
/// <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)
public static async ValueTask<string> CompressToCacheAsync(string cachesFolderPath, Guid assetId, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
{
var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER);
Directory.CreateDirectory(cacheDir);
@@ -57,129 +183,11 @@ internal static unsafe class TextureProcessor
File.Delete(stale);
}
RunNvttPipeline(cachePath, pixelData, width, height, isFloat, colorComponents, settings);
return cachePath;
}
private static void RunNvttPipeline(
string outputPath,
ReadOnlySpan<byte> pixelData,
int width,
int height,
bool isFloat,
ColorComponents colorComponents,
TextureAssetSettings settings)
{
using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create());
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
using var pOutOpts = new DisposablePtr<NvttOutputOptions>(NvttOutputOptions.Create());
using var pCtx = new DisposablePtr<NvttContext>(NvttContext.Create());
var inputFormat = isFloat
? NvttInputFormat.NVTT_InputFormat_RGBA_32F
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
fixed (void* pData = pixelData)
{
pSurface.Get()->SetImageData(inputFormat, width, height, 1, pData, NvttBoolean.NVTT_True, null);
}
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA,
// so channels R and B are swapped — fix with swizzle(2,1,0,3).
if (!isFloat)
{
pSurface.Get()->Swizzle(2, 1, 0, 3, null);
}
var maxExtent = (int)settings.Sampler.MaxSize;
if (settings.Advanced.StretchToPowerOfTwo)
{
pSurface.Get()->ResizeMakeSquare(maxExtent,
NvttRoundMode.NVTT_RoundMode_ToNearestPowerOfTwo,
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
}
else if (pSurface.Get()->Width() > maxExtent || pSurface.Get()->Height() > maxExtent)
{
pSurface.Get()->ResizeMax(maxExtent,
NvttRoundMode.NVTT_RoundMode_None,
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
}
if (settings.Advanced.UseBorderColor)
{
var c = settings.Advanced.BorderColor;
pSurface.Get()->SetBorder(c.r, c.g, c.b, c.a, null);
}
else if (settings.Advanced.ZeroAlphaBorder)
{
pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null);
}
var workItem = new NvttPipelineTask(cachePath, image, width, height, depth, settings);
ThreadPool.UnsafeQueueUserWorkItem(workItem, true);
await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
if (settings.Basic.IsSRGB && settings.Advanced.GammaCorrection)
{
pSurface.Get()->ToLinearFromSrgb(null);
}
if (settings.Advanced.PremultiplyAlpha)
{
pSurface.Get()->PremultiplyAlpha(null);
}
pCompOpts.Get()->SetFormat(SelectFormat(settings));
pCompOpts.Get()->SetQuality(SelectQuality(settings.Advanced.CompressionLevel));
if (settings.Advanced.CutoutAlpha)
{
pCompOpts.Get()->SetQuantization(false, false, true,
settings.Advanced.CutoutAlphaThreshold);
}
pOutOpts.Get()->SetOutputHeader(true);
pOutOpts.Get()->SetSrgbFlag(settings.Basic.IsSRGB);
pOutOpts.Get()->SetContainer(NvttContainer.NVTT_Container_DDS10);
pOutOpts.Get()->SetFileName(Encoding.UTF8.GetBytes(outputPath));
var nvttFilter = SelectMipmapFilter(settings.Advanced.MipmapFilter);
int mipmapCount;
if (!settings.Advanced.GenerateMipmaps)
{
mipmapCount = 1;
}
else if (settings.Advanced.MipmapLevelCount == 0)
{
mipmapCount = pSurface.Get()->CountMipmaps(1);
}
else
{
mipmapCount = (int)settings.Advanced.MipmapLevelCount;
}
pCtx.Get()->SetCudaAcceleration(NvttApi.IsCudaSupported());
pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get());
using var pMip = new DisposablePtr<NvttSurface>(pSurface.Get()->Clone());
for (var level = 0; level < mipmapCount; level++)
{
// Scale alpha for coverage on each pMip (if requested)
if (settings.Advanced.ScaleAlphaForMipCoverage && level > 0)
{
var refCoverage = pMip.Get()->AlphaTestCoverage(
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3);
pMip.Get()->ScaleAlphaToCoverage(refCoverage,
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null);
}
pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get());
if (level + 1 < mipmapCount)
{
pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null);
}
}
return cachePath;
}
private static NvttFormat SelectFormat(TextureAssetSettings settings)
@@ -208,7 +216,7 @@ internal static unsafe class TextureProcessor
_ => NvttMipmapFilter.NVTT_MipmapFilter_Kaiser,
};
private static ulong ComputeSettingsHash(TextureAssetSettings s)
private static ulong ComputeSettingsHash(TextureAssetSettings settings)
{
var basicSize = Unsafe.SizeOf<TextureAssetSettings.BasicSettings>();
var advancedSize = Unsafe.SizeOf<TextureAssetSettings.AdvancedSettings>();
@@ -216,9 +224,9 @@ internal static unsafe class TextureProcessor
var total = basicSize + advancedSize + samplerSize;
Span<byte> buf = stackalloc byte[total];
var basic = s.Basic;
var advanced = s.Advanced;
var sampler = s.Sampler;
var basic = settings.Basic;
var advanced = settings.Advanced;
var sampler = settings.Sampler;
MemoryMarshal.Write(buf, in basic);
MemoryMarshal.Write(buf.Slice(basicSize), in advanced);
MemoryMarshal.Write(buf.Slice(basicSize + advancedSize), in sampler);

View File

@@ -8,7 +8,7 @@ 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 LIBRARY_FOLDER_NAME = "Library";
public const string CONFIG_FOLDER_NAME = "Config";
private static IServiceProvider? s_serviceProvider;
@@ -25,7 +25,7 @@ public static class EditorApplication
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 LibraryFolderPath => Path.Combine(ProjectPath, LIBRARY_FOLDER_NAME);
public static string ConfigFolderPath => Path.Combine(ProjectPath, CONFIG_FOLDER_NAME);
public static DispatcherQueue DispatcherQueue

View File

@@ -3,7 +3,8 @@
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Editor.Core</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<Platforms>x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
@@ -13,8 +14,9 @@
<langversion>preview</langversion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-HDRI-OpenMP-x64" Version="14.12.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />

View File

@@ -3,8 +3,8 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<Platforms>x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
@@ -41,7 +41,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
</ItemGroup>