feat(asset): asset manager + registration generator

Add runtime AssetManager and AssetHandlerRegistrationGenerator source generator.
Update editor asset handler types and services to work with new registration
mechanism and asset catalog. Remove legacy contracts ICloneable and IReleasable.

Files added:
- src/Runtime/Ghost.Engine/AssetManager.cs
- src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs

Major edits:
- Editor asset handler classes and services (Asset*, Texture*, Registry)
- Runtime Handle.cs and project files
- Render graph executor and tests updated accordingly

This commit introduces the foundation for the modern asset pipeline
including generated registration of asset handlers and a centralized
runtime AssetManager that will drive asset lifecycle.
This commit is contained in:
2026-04-15 22:21:00 +09:00
parent 6615fe794e
commit 13bf1501e4
29 changed files with 425 additions and 365 deletions

View File

@@ -8,7 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1" /> <PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1" />
<PackageReference Include="Antlr4BuildTasks" Version="12.11.0" /> <PackageReference Include="Antlr4BuildTasks" Version="12.14.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,6 +1,4 @@
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.AssetHandler;
@@ -16,21 +14,9 @@ public abstract class Asset
get; get;
} }
public Guid[] Dependencies protected Asset(Guid id)
{
get;
}
public IAssetSettings? Settings
{
get;
}
protected Asset(Guid id, Guid[] dependencies, IAssetSettings? settings)
{ {
ID = id; ID = id;
Dependencies = dependencies;
Settings = settings;
} }
public virtual ValueTask RefreshAsync(IAssetRegistry db, CancellationToken token = default) public virtual ValueTask RefreshAsync(IAssetRegistry db, CancellationToken token = default)
@@ -39,100 +25,6 @@ public abstract class Asset
} }
} }
// Do not change the order of the fields in this struct, as it is used for binary serialization/deserialization.
[StructLayout(LayoutKind.Sequential, Size = SIZE)]
internal struct AssetMetadata
{
public const int CURRENT_FORMAT_VERSION = 1;
public const int SIZE = 128; // Fixed size for metadata header. We choose 128 bytes to allow future expansion without breaking compatibility.
public AssetMetadata(Guid id, Guid typeID)
{
FormatVersion = CURRENT_FORMAT_VERSION;
ID = id;
TypeID = typeID;
}
public int FormatVersion
{
get;
}
public Guid ID
{
get;
}
public Guid TypeID
{
get;
}
public int HandlerVersion
{
get; set;
}
public int DependencyCount
{
get; set;
}
public long DependenciesOffset
{
get; set;
}
public long SettingsOffset
{
get; set;
}
public long SettingsSize
{
get; set;
}
public long ContentOffset
{
get; set;
}
public long ContentSize
{
get; set;
}
public static void WriteToStream(Stream stream, scoped ref readonly AssetMetadata metadata)
{
var buffer = MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in metadata, 1));
stream.Write(buffer);
}
public static AssetMetadata ReadFromStream(Stream stream)
{
Span<byte> buffer = stackalloc byte[SIZE];
stream.ReadExactly(buffer);
return Unsafe.ReadUnaligned<AssetMetadata>(ref MemoryMarshal.GetReference(buffer));
}
}
[StructLayout(LayoutKind.Sequential, Size = SIZE)]
public readonly struct DependencyInfo
{
public const int SIZE = 16;
public Guid ID
{
get; init;
}
public readonly ReadOnlySpan<byte> AsBytes()
{
return MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1));
}
}
public readonly struct AssetReference : IEquatable<AssetReference> public readonly struct AssetReference : IEquatable<AssetReference>
{ {
private readonly int _value; private readonly int _value;

View File

@@ -6,34 +6,27 @@ namespace Ghost.Editor.Core.AssetHandler;
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public sealed class CustomAssetHandlerAttribute : Attribute public sealed class CustomAssetHandlerAttribute : Attribute
{ {
public required string ID public CustomAssetHandlerAttribute(string id, string[] supportedExtensions, int version = 1)
{ {
get; init;
} }
public required string[] SupportedExtensions
{
get; init;
}
public bool AllowCaching
{
get; init;
} = true;
} }
public interface IAssetExportOptions; public interface IAssetExportOptions;
public interface IAssetHandler public interface IAssetHandler
{ {
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default); ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, Guid id, IAssetRegistry assetRegistry, CancellationToken token = default);
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default); ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default);
} }
public interface IImportableAssetHandler : IAssetHandler public interface IImportableAssetHandler : IAssetHandler
{ {
IAssetSettings? CreateDefaultSettings() => null; IAssetSettings? CreateDefaultSettings();
ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default);
}
public interface IExportableAssetHandler : IAssetHandler
{
ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default); ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default);
} }
@@ -46,16 +39,16 @@ public static class AssetHandlerExtensions
return await handler.ImportAsync(sourceStream, targetStream, id, settings, token); return await handler.ImportAsync(sourceStream, targetStream, id, settings, token);
} }
public static async ValueTask<Result> ExportAsync(this IImportableAssetHandler handler, string assetFilePath, string targetFilePath, IAssetExportOptions? options, CancellationToken token = default) public static async ValueTask<Result> ExportAsync(this IExportableAssetHandler handler, string assetFilePath, string targetFilePath, IAssetExportOptions? options, CancellationToken token = default)
{ {
await using var assetStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); await using var assetStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await using var targetStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None); await using var targetStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
return await handler.ExportAsync(assetStream, targetStream, options, token); return await handler.ExportAsync(assetStream, targetStream, options, token);
} }
public static async ValueTask<Result<Asset>> LoadAsync(this IAssetHandler handler, string assetFilePath, IAssetRegistry assetDatabase, CancellationToken token = default) public static async ValueTask<Result<Asset>> LoadAsync(this IAssetHandler handler, string assetFilePath, Guid id, IAssetRegistry assetDatabase, CancellationToken token = default)
{ {
await using var sourceStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); await using var sourceStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return await handler.LoadAsync(sourceStream, assetDatabase, token); return await handler.LoadAsync(sourceStream, id, assetDatabase, token);
} }
} }

View File

@@ -1,13 +1,10 @@
using System.Reflection;
using Ghost.Editor.Core.Utilities;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.AssetHandler;
/// <summary> /// <summary>
/// One-time scan at editor startup → two dictionaries. /// One-time scan at editor startup → two dictionaries.
/// All lookups are O(1) after construction. /// All lookups are O(1) after construction.
/// </summary> /// </summary>
internal sealed class AssetHandlerRegistry public sealed class AssetHandlerRegistry
{ {
private readonly Dictionary<string, IAssetHandler> _byExtension; private readonly Dictionary<string, IAssetHandler> _byExtension;
private readonly Dictionary<Guid, IAssetHandler> _byTypeId; private readonly Dictionary<Guid, IAssetHandler> _byTypeId;
@@ -18,51 +15,19 @@ internal sealed class AssetHandlerRegistry
_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase); _byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase);
_byTypeId = new Dictionary<Guid, IAssetHandler>(); _byTypeId = new Dictionary<Guid, IAssetHandler>();
_versionByTypeId = new Dictionary<Guid, int>(); _versionByTypeId = new Dictionary<Guid, int>();
foreach (var typeInfo in TypeCache.GetTypes())
{
if (typeInfo.IsAbstract || typeInfo.IsInterface)
{
continue;
} }
if (!typeof(IAssetHandler).IsAssignableFrom(typeInfo)) public void RegisterHandler(IAssetHandler handler, ReadOnlySpan<string> extensions, Guid typeId, int version)
{
continue;
}
var attr = typeInfo.GetCustomAttribute<CustomAssetHandlerAttribute>();
if (attr == null)
{
continue;
}
if (!Guid.TryParse(attr.ID, out var typeId))
{
continue;
}
try
{
if (Activator.CreateInstance(typeInfo) is IAssetHandler handler)
{ {
_byTypeId[typeId] = handler; _byTypeId[typeId] = handler;
// Note: Versioning could be expanded, but for now we assume version 1 or look for a constant _versionByTypeId[typeId] = version;
_versionByTypeId[typeId] = 1;
foreach (var ext in attr.SupportedExtensions) foreach (var ext in extensions)
{ {
var normalizedExt = ext.StartsWith('.') ? ext : "." + ext; var normalizedExt = ext.StartsWith('.') ? ext : "." + ext;
_byExtension[normalizedExt] = handler; _byExtension[normalizedExt] = handler;
} }
} }
}
catch
{
// Log failure to instantiate handler in real app
}
}
}
public IAssetHandler? GetByExtension(string extension) public IAssetHandler? GetByExtension(string extension)
{ {

View File

@@ -67,6 +67,8 @@ public sealed class AssetMeta
internal static class AssetMetaIO internal static class AssetMetaIO
{ {
private const string _META_EXTENSION = ".gmeta";
private static readonly JsonSerializerOptions s_options = new() private static readonly JsonSerializerOptions s_options = new()
{ {
WriteIndented = true, WriteIndented = true,
@@ -108,7 +110,7 @@ internal static class AssetMetaIO
File.Move(tempPath, metaPath); File.Move(tempPath, metaPath);
} }
public static string GetMetaPath(string sourceFilePath) => sourceFilePath + ".gmeta"; public static string GetMetaPath(string sourceFilePath) => sourceFilePath + _META_EXTENSION;
public static string GetSourcePath(string metaPath) => metaPath[..^".gmeta".Length]; public static string GetSourcePath(string metaPath) => metaPath[..^_META_EXTENSION.Length];
} }

View File

@@ -2,10 +2,7 @@ using Ghost.Core;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using ImageMagick; using ImageMagick;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using static Ghost.Editor.Core.AssetHandler.TextureAssetSettings;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.AssetHandler;
@@ -54,15 +51,47 @@ public class TextureAsset : Asset
internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F"; internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F";
internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID); internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID);
private readonly Handle<GPUTexture> _texture; private readonly byte[] _textureData;
private readonly uint _width;
private readonly uint _height;
private readonly uint _depth;
private readonly uint _colorComponents;
public override Guid TypeID => s_typeGuid; public override Guid TypeID => s_typeGuid;
public Handle<GPUTexture> Texture => _texture;
public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings, Handle<GPUTexture> texture) /// <summary>
: base(id, dependencies, settings) /// Gets the raw texture data in a compressed format.
/// </summary>
public ReadOnlyMemory<byte> TextureData => _textureData;
/// <summary>
/// Gets the width of the texture in pixels.
/// </summary>
public uint Width => _width;
/// <summary>
/// Gets the height of the texture in pixels.
/// </summary>
public uint Height => _height;
/// <summary>
/// Gets the bit depth of the texture.
/// </summary>
public uint Depth => _depth;
/// <summary>
/// Gets the number of color components in the texture.
/// </summary>
public uint ColorComponents => _colorComponents;
internal TextureAsset(byte[] data, ImageContentHeader header, Guid id)
: base(id)
{ {
_texture = texture; _textureData = data;
_width = header.width;
_height = header.height;
_depth = header.depth;
_colorComponents = header.colorComponents;
} }
} }
@@ -213,12 +242,8 @@ public class TextureAssetSettings : IAssetSettings
} = new SamplerSettings(); } = new SamplerSettings();
} }
[CustomAssetHandler(ID = TextureAsset._TYPE_ID, SupportedExtensions = new[] { ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" })] [StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility
internal class TextureAssetHandler : IImportableAssetHandler internal struct ImageContentHeader
{
private const int _CURRENT_VERSION = 1;
private struct ImageContentHeader
{ {
public uint width; public uint width;
public uint height; public uint height;
@@ -226,107 +251,68 @@ internal class TextureAssetHandler : IImportableAssetHandler
public uint colorComponents; public uint colorComponents;
} }
private static async ValueTask<Result<long>> WriteSettingsToStreamAsync(TextureAssetSettings settings, Stream stream, CancellationToken token = default) [CustomAssetHandler(TextureAsset._TYPE_ID, [ ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" ], 1)]
internal class TextureAssetHandler : IImportableAssetHandler
{ {
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>(); public IAssetSettings? CreateDefaultSettings()
var tempArray = ArrayPool<byte>.Shared.Rent(size); {
return new TextureAssetSettings();
}
public async ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, Guid id, IAssetRegistry assetRegistry, CancellationToken token = default)
{
try try
{ {
ref var address = ref MemoryMarshal.GetReference(tempArray); // FIX: Should the sourceStream be the stream of the imported file or the raw asset file?
Unsafe.WriteUnaligned(ref address, settings.Basic); // Or should we change our paramemters to inlcude more information and let each handler decide how to load the asset?
Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>()), settings.Advanced); // The problem of a single sourceStream is, for example, for texture assets, we don't even need to read the ".png" file at all,
Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()), settings.Sampler); // but for some other asset types, we may don't even have imported intermediate files at all.
await stream.WriteAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false); // var path = assetRegistry.GetAssetPath(id);
// if (string.IsNullOrEmpty(path))
// {
// return Result.Failure("Asset path not found in registry.");
// }
//
// var metadataPath = AssetMetaIO.GetMetaPath(path);
// var meta = await AssetMetaIO.ReadAsync(metadataPath, token).ConfigureAwait(false);
// Logger.DebugAssert(meta != null, $"Missing or invalid metadata for asset at {path}");
return Result.Success<long>(size);
var header = new ImageContentHeader();
sourceStream.ReadExactly(MemoryMarshal.AsBytes(new Span<ImageContentHeader>(ref header)));
var imageDataSize = (int)(sourceStream.Length - sourceStream.Position);
var imageData = new byte[imageDataSize];
await sourceStream.ReadExactlyAsync(imageData, token).ConfigureAwait(false);
var asset = new TextureAsset(imageData, header, id);
return asset;
} }
catch (Exception ex) catch (Exception ex)
{ {
return Result.Failure($"Failed to write texture asset settings to stream: {ex.Message}"); return Result.Failure($"Failed to load texture asset: {ex.Message}");
}
finally
{
ArrayPool<byte>.Shared.Return(tempArray);
} }
} }
private static async ValueTask<Result<IAssetSettings>> ReadSettingsFromStreamAsync(Stream stream, CancellationToken token = default) public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default)
{
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
var tempArray = ArrayPool<byte>.Shared.Rent(size);
try
{
await stream.ReadAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false);
// 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
{
Basic = basic,
Advanced = advanced,
Sampler = sampler
};
return Result.Success<IAssetSettings>(settings);
}
catch (Exception ex)
{
return Result.Failure($"Failed to read texture asset settings from stream: {ex.Message}");
}
finally
{
ArrayPool<byte>.Shared.Return(tempArray);
}
}
public ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
try
{ {
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
using var image = new MagickImage(sourceStream); using var image = new MagickImage(sourceStream);
var bytes = image.ToByteArray(); var bytes = image.ToByteArray();
await TextureProcessor.CompressToCacheAsync(EditorApplication.LibraryFolderPath, id, bytes, image.Width, image.Height, image.Depth, textureSettings, token).ConfigureAwait(false); await TextureProcessor.CompressToCacheAsync(EditorApplication.LibraryFolderPath, id, bytes, image.Width, image.Height, image.Depth, textureSettings, token)
.ConfigureAwait(false);
var header = new AssetMetadata(id, TextureAsset.s_typeGuid) targetStream.Seek(0, SeekOrigin.Begin);
{
HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE,
};
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
var sizeResult = await WriteSettingsToStreamAsync(textureSettings, targetStream, token).ConfigureAwait(false);
if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
}
// Content layout (all little-endian):
// uint32 width
// uint32 height
// uint32 depth
// uint32 colorComponents
// byte[] pixelBytes
header.SettingsSize = sizeResult.Value;
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
unsafe
{
header.ContentSize = sizeof(ImageContentHeader) + image.Width * image.Height * (image.Depth / 8) * image.ChannelCount;
}
// Write raw image content
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var contentHeader = new ImageContentHeader var contentHeader = new ImageContentHeader
{ {
@@ -341,20 +327,11 @@ internal class TextureAssetHandler : IImportableAssetHandler
await targetStream.WriteAsync(bytes, token).ConfigureAwait(false); await targetStream.WriteAsync(bytes, token).ConfigureAwait(false);
await targetStream.FlushAsync(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(); return Result.Success();
} }
catch (Exception ex)
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)
{ {
throw new NotImplementedException(); return Result.Failure($"Failed to import texture asset: {ex.Message}");
} }
public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default)
{
throw new NotImplementedException();
} }
} }

View File

@@ -57,8 +57,20 @@ public class EditorInjectionAttribute : DiscoverableAttributeBase
Transient, Transient,
} }
public ServiceLifetime Lifetime
{
get;
}
public Type ImplementationType
{
get;
}
public EditorInjectionAttribute(ServiceLifetime lifetime, Type implementationType) public EditorInjectionAttribute(ServiceLifetime lifetime, Type implementationType)
{ {
Lifetime = lifetime;
ImplementationType = implementationType;
} }
} }

View File

@@ -15,7 +15,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Magick.NET-Q16-HDRI-OpenMP-x64" Version="14.12.0" /> <PackageReference Include="Magick.NET-Q16-HDRI-OpenMP-x64" Version="14.12.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.6" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
@@ -25,6 +25,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\..\Runtime\Ghost.Generator\Ghost.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\ThridParty\Ghost.DXC\Ghost.DXC.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.DXC\Ghost.DXC.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -224,6 +224,19 @@ internal sealed class AssetCatalog : IDisposable
return list; return list;
} }
public List<Guid> GetDependencies(Guid guid)
{
_cmdGetDependencies.Parameters.Clear();
_cmdGetDependencies.Parameters.AddWithValue("@guid", guid.ToByteArray());
using var reader = _cmdGetDependencies.ExecuteReader();
var list = new List<Guid>();
while (reader.Read())
{
list.Add(new Guid((byte[])reader[0]));
}
return list;
}
public List<(Guid guid, string sourcePath)> GetDirtyAssets() public List<(Guid guid, string sourcePath)> GetDirtyAssets()
{ {
using var reader = _cmdGetDirty.ExecuteReader(); using var reader = _cmdGetDirty.ExecuteReader();

View File

@@ -165,11 +165,11 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
return; return;
} }
var handlerTypeId = handler?.GetType().GetCustomAttribute<CustomAssetHandlerAttribute>()?.ID; var handlerTypeId = handler?.GetType().GetCustomAttributesData().FirstOrDefault(d => d.AttributeType == typeof(CustomAssetHandlerAttribute))?.ConstructorArguments[0].Value;
var meta = new AssetMeta var meta = new AssetMeta
{ {
Guid = Guid.NewGuid(), Guid = Guid.NewGuid(),
HandlerTypeId = handlerTypeId == null ? null : Guid.Parse(handlerTypeId), HandlerTypeId = handlerTypeId is string str? Guid.Parse(str) : null,
HandlerVersion = 1, HandlerVersion = 1,
Settings = importable?.CreateDefaultSettings() Settings = importable?.CreateDefaultSettings()
}; };
@@ -182,9 +182,15 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, relativePath, metaPath, ImportReason.NewAsset)); await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, relativePath, metaPath, ImportReason.NewAsset));
} }
public string? GetAssetPath(Guid id) => _catalog.GetSourcePath(id); public string? GetAssetPath(Guid id)
{
return _catalog.GetSourcePath(id);
}
public Guid GetAssetGuid(string path) => _catalog.GetGuid(path.Replace('\\', '/')); public Guid GetAssetGuid(string path)
{
return _catalog.GetGuid(path.Replace(Path.DirectorySeparatorChar, '/'));
}
public async ValueTask<Result<Guid>> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default) public async ValueTask<Result<Guid>> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default)
{ {
@@ -192,7 +198,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
// Current requirement: "returns the new GUID immediately (import happens in background)" // Current requirement: "returns the new GUID immediately (import happens in background)"
var ext = Path.GetExtension(sourceFilePath); var ext = Path.GetExtension(sourceFilePath);
var relativePath = targetAssetPath.Replace('\\', '/'); var relativePath = targetAssetPath.Replace(Path.DirectorySeparatorChar, '/');
var fullPath = Path.Combine(_assetsRoot, relativePath); var fullPath = Path.Combine(_assetsRoot, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
@@ -224,7 +230,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
{ {
if (_loadedAssets.TryGetValue(id, out var weakRef) && weakRef.TryGetTarget(out var asset)) if (_loadedAssets.TryGetValue(id, out var weakRef) && weakRef.TryGetTarget(out var asset))
{ {
return Result.Success(asset); return asset;
} }
await _loadLock.WaitAsync(token); await _loadLock.WaitAsync(token);
@@ -232,7 +238,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
{ {
if (_loadedAssets.TryGetValue(id, out weakRef) && weakRef.TryGetTarget(out asset)) if (_loadedAssets.TryGetValue(id, out weakRef) && weakRef.TryGetTarget(out asset))
{ {
return Result.Success(asset); return asset;
} }
var importedPath = Path.Combine(_libraryRoot, "Imports", $"{id:N}.imported"); var importedPath = Path.Combine(_libraryRoot, "Imports", $"{id:N}.imported");
@@ -257,7 +263,10 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
} }
} }
public ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default) => throw new NotImplementedException(); public ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default)
{
throw new NotImplementedException();
}
public void Dispose() public void Dispose()
{ {

View File

@@ -97,7 +97,7 @@ internal sealed class ImportCoordinator : IDisposable
return; return;
} }
var handler = (meta.HandlerTypeId.HasValue) var handler = meta.HandlerTypeId.HasValue
? _handlers.GetByTypeId(meta.HandlerTypeId.Value) ? _handlers.GetByTypeId(meta.HandlerTypeId.Value)
: _handlers.GetByExtension(Path.GetExtension(job.SourcePath)); : _handlers.GetByExtension(Path.GetExtension(job.SourcePath));

View File

@@ -6,13 +6,13 @@ namespace Ghost.Editor.Core.Utilities;
public static class TypeCache public static class TypeCache
{ {
private static TypeInfo[] s_types; private static TypeInfo[] s_types = null!;
private static Dictionary<nint, List<MethodInfo>> s_attributeMethodCache; private static Dictionary<nint, List<MethodInfo>> s_attributeMethodCache = null!;
private static Dictionary<nint, List<int>> s_attributeTypeCache = null!;
static TypeCache() static TypeCache()
{ {
s_types = LoadTypes(); Reload();
s_attributeMethodCache = FindMethodWithAttribute();
} }
private static TypeInfo[] LoadTypes() private static TypeInfo[] LoadTypes()
@@ -62,6 +62,29 @@ public static class TypeCache
return dict; return dict;
} }
private static Dictionary<nint, List<int>> FindTypesWithAttribute()
{
var dict = new Dictionary<nint, List<int>>();
for (int i = 0; i < s_types.Length; i++)
{
TypeInfo? type = s_types[i];
var attrs = type.GetCustomAttributes<DiscoverableAttributeBase>(false);
foreach (var attr in attrs)
{
var key = attr.GetType().TypeHandle.Value;
ref var typeList = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out var exist);
if (!exist)
{
typeList = new List<int>();
}
typeList!.Add(i);
}
}
return dict;
}
internal static void Initialize() internal static void Initialize()
{ {
// Intentionally left blank. // Intentionally left blank.
@@ -72,6 +95,7 @@ public static class TypeCache
{ {
s_types = LoadTypes(); s_types = LoadTypes();
s_attributeMethodCache = FindMethodWithAttribute(); s_attributeMethodCache = FindMethodWithAttribute();
s_attributeTypeCache = FindTypesWithAttribute();
} }
public static IReadOnlyCollection<TypeInfo> GetTypes() public static IReadOnlyCollection<TypeInfo> GetTypes()
@@ -79,7 +103,7 @@ public static class TypeCache
return s_types; return s_types;
} }
public static IReadOnlyCollection<MethodInfo>? GetMethodsWithAttribute<T>() public static IEnumerable<MethodInfo>? GetMethodsWithAttribute<T>()
where T : DiscoverableAttributeBase where T : DiscoverableAttributeBase
{ {
var key = typeof(T).TypeHandle.Value; var key = typeof(T).TypeHandle.Value;
@@ -90,4 +114,16 @@ public static class TypeCache
return null; return null;
} }
public static IEnumerable<TypeInfo>? GetTypesWithAttribute<T>()
where T : DiscoverableAttributeBase
{
var key = typeof(T).TypeHandle.Value;
if (s_attributeTypeCache.TryGetValue(key, out var typeIndices))
{
return typeIndices.Select(i => s_types[i]);
}
return null;
}
} }

View File

@@ -72,6 +72,7 @@ public partial class App : Application
services.AddTransient<ProjectBrowserViewModel>(); services.AddTransient<ProjectBrowserViewModel>();
// TODO: Use source generators to generate this code at compile time instead of using reflection at runtime.
foreach (var type in TypeCache.GetTypes()) foreach (var type in TypeCache.GetTypes())
{ {
var data = type.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType == typeof(EditorInjectionAttribute)); var data = type.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType == typeof(EditorInjectionAttribute));

View File

@@ -39,7 +39,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.6" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" /> <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />

View File

@@ -1,11 +0,0 @@
namespace Ghost.Core.Contracts;
public interface ICloneable
{
object Clone();
}
public interface ICloneable<T>
{
T Clone();
}

View File

@@ -1,6 +0,0 @@
namespace Ghost.Core.Contracts;
internal interface IReleasable
{
void InternalRelease();
}

View File

@@ -26,7 +26,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" /> <PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" />
<PackageReference Include="System.IO.Hashing" Version="10.0.5" /> <PackageReference Include="System.IO.Hashing" Version="10.0.6" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" /> <PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
</ItemGroup> </ItemGroup>

View File

@@ -1,3 +1,5 @@
using System.Runtime.InteropServices;
namespace Ghost.Core; namespace Ghost.Core;
public readonly struct Handle<T> : IEquatable<Handle<T>> public readonly struct Handle<T> : IEquatable<Handle<T>>
@@ -246,3 +248,48 @@ public readonly struct Key128<T> : IEquatable<Key128<T>>
public static implicit operator UInt128(Key128<T> key) => key.Value; public static implicit operator UInt128(Key128<T> key) => key.Value;
public static implicit operator Key128<T>(UInt128 value) => new Key128<T>(value); public static implicit operator Key128<T>(UInt128 value) => new Key128<T>(value);
} }
[StructLayout(LayoutKind.Sequential)]
public readonly struct AssetRef<T> : IEquatable<AssetRef<T>>
{
public readonly Guid guid;
public static AssetRef<T> Null => default;
public bool IsValid => guid != Guid.Empty;
public AssetRef(Guid guid)
{
this.guid = guid;
}
public bool Equals(AssetRef<T> other)
{
return guid == other.guid;
}
public override int GetHashCode()
{
return guid.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is AssetRef<T> r && Equals(r);
}
public override string ToString()
{
return $"AssetRef<{typeof(T).Name}>({guid:N})";
}
public static bool operator ==(AssetRef<T> a, AssetRef<T> b)
{
return a.Equals(b);
}
public static bool operator !=(AssetRef<T> a, AssetRef<T> b)
{
return !a.Equals(b);
}
}

View File

@@ -0,0 +1,44 @@
using Ghost.Core;
using System.Runtime.InteropServices;
namespace Ghost.Engine;
internal abstract class RuntimeAsset;
internal interface IRuntimeAssetLoader
{
ValueTask<Result<RuntimeAsset>> LoadAsync(Stream cookedData, Guid id, CancellationToken token = default);
}
internal sealed class RuntimeLoaderRegistry
{
private readonly Dictionary<Guid, IRuntimeAssetLoader> _loaders = new();
public void Register(Guid cookedTypeId, IRuntimeAssetLoader loader)
{
_loaders[cookedTypeId] = loader;
}
public IRuntimeAssetLoader? GetLoader(Guid cookedTypeId)
{
_loaders.TryGetValue(cookedTypeId, out var loader);
return loader;
}
}
internal sealed class CookedTextureLoader : IRuntimeAssetLoader
{
public static readonly Guid TYPE_ID = TextureAsset.s_typeGuid;
public async ValueTask<Result<RuntimeAsset>> LoadAsync(Stream cookedData, Guid id, CancellationToken token)
{
// Read the ImageContentHeader you wrote during import
var header = new ImageContentHeader();
cookedData.ReadExactly(MemoryMarshal.AsBytes(new Span<ImageContentHeader>(ref header)));
// Read the rest as raw GPU data (DDS/BC compressed bytes)
var data = new byte[cookedData.Length - cookedData.Position];
await cookedData.ReadExactlyAsync(data, token);
return new TextureAsset(data, header, id);
}
}
public class AssetManager
{
}

View File

@@ -0,0 +1,90 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
namespace Ghost.Generator;
[Generator]
internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var handerCandidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is ClassDeclarationSyntax,
transform: GetAssetHandlerSymbol)
.Where(symbol => symbol != null)
.Collect();
context.RegisterSourceOutput(handerCandidates, GenerateRegistrationCode);
}
private void GenerateRegistrationCode(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> array)
{
if (array.IsDefaultOrEmpty)
{
return;
}
var sb = new System.Text.StringBuilder();
foreach (var symbol in array)
{
var attribute = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "Ghost.Editor.Core.AssetHandler.CustomAssetHandlerAttribute");
if (attribute == null)
{
continue;
}
var id = attribute.ConstructorArguments[0].Value as string;
var extensionsTypesConstants = attribute.ConstructorArguments[1].Values;
var extensions = $"new string[] {{ {string.Join(", ", extensionsTypesConstants.Select(v => v.ToCSharpString()))} }}";
var version = (int)attribute.ConstructorArguments[2].Value;
sb.Append($" global::Ghost.Editor.Core.AssetHandler.AssetHandlerRegistry.RegisterHandler(new {symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}(), Guid.Parse(\"{id}\"), {extensions}, {version});");
}
var registerTypeName = "g_assethandler_registeration";
var code = $@"// <auto-generated />
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName}
{{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void RegisterAssetHandlers()
{{
{sb}
}}
}}";
context.AddSource($"{registerTypeName}.gen.cs", code);
}
private INamedTypeSymbol GetAssetHandlerSymbol(GeneratorSyntaxContext context, CancellationToken token)
{
var classSyntax = (ClassDeclarationSyntax)context.Node;
if (context.SemanticModel.GetDeclaredSymbol(classSyntax) is not INamedTypeSymbol symbol)
{
return null;
}
var iHandlerSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("Ghost.Editor.Core.AssetHandler.IAssetHandler");
if (iHandlerSymbol == null)
{
return null;
}
foreach (var iface in symbol.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(iface, iHandlerSymbol))
{
return symbol;
}
}
return null;
}
}

View File

@@ -1,6 +1,5 @@
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -14,7 +13,6 @@ namespace Ghost.Generator
{ {
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)
{ {
// 1. Pipeline: Find all structs implementing IComponent
var componentCandidates = context.SyntaxProvider var componentCandidates = context.SyntaxProvider
.CreateSyntaxProvider( .CreateSyntaxProvider(
predicate: (s, _) => s is StructDeclarationSyntax, predicate: (s, _) => s is StructDeclarationSyntax,
@@ -22,7 +20,6 @@ namespace Ghost.Generator
.Where(symbol => symbol != null) .Where(symbol => symbol != null)
.Collect(); .Collect();
// 4. Output: Generate the source using both pieces of data at once
context.RegisterSourceOutput(componentCandidates, GenerateRegistrationCode); context.RegisterSourceOutput(componentCandidates, GenerateRegistrationCode);
} }
@@ -30,7 +27,7 @@ namespace Ghost.Generator
private static INamedTypeSymbol GetComponentSymbol(GeneratorSyntaxContext ctx, CancellationToken _) private static INamedTypeSymbol GetComponentSymbol(GeneratorSyntaxContext ctx, CancellationToken _)
{ {
var structSyntax = (StructDeclarationSyntax)ctx.Node; var structSyntax = (StructDeclarationSyntax)ctx.Node;
if (!(ctx.SemanticModel.GetDeclaredSymbol(structSyntax) is INamedTypeSymbol symbol)) if (ctx.SemanticModel.GetDeclaredSymbol(structSyntax) is not INamedTypeSymbol symbol)
{ {
return null; return null;
} }

View File

@@ -13,11 +13,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="TerraFX.Interop.D3D12MemoryAllocator" Version="3.1.0" /> <PackageReference Include="TerraFX.Interop.D3D12MemoryAllocator" Version="3.1.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,7 +1,6 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services; using Ghost.Graphics.Services;
using System.Diagnostics;
namespace Ghost.Graphics.RenderGraphModule; namespace Ghost.Graphics.RenderGraphModule;

View File

@@ -42,11 +42,11 @@
<ProjectCapability Include="Msix" /> <ProjectCapability Include="Msix" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.0.1" /> <PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.4.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" /> <PackageReference Include="MSTest.TestAdapter" Version="4.2.1" />
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" /> <PackageReference Include="MSTest.TestFramework" Version="4.2.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />

View File

@@ -5,13 +5,12 @@ using Ghost.Engine.Components;
using Ghost.Engine.Systems; using Ghost.Engine.Systems;
using Ghost.Entities; using Ghost.Entities;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.Test.RenderPipeline;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics.Test.Systems; namespace Ghost.Graphics.Test.Systems;
[RenderPipelineSystem<TestRenderPipelineSettings>] //[RenderPipelineSystem<TestRenderPipelineSettings>]
[UpdateAfter<CameraMovingSystem>] [UpdateAfter<CameraMovingSystem>]
public class RenderExtractionSystem : ISystem public class RenderExtractionSystem : ISystem
{ {

View File

@@ -10,8 +10,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.6" />
<PackageReference Include="MSTest" Version="4.0.1" /> <PackageReference Include="MSTest" Version="4.2.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>