Refactor component registration, update deps, improve JSON

- Updated Misaki.HighPerformance package versions in Core and Graphics projects.
- Added IsTrimmable to Ghost.Engine.csproj for trimming support.
- Renamed GetOrRegisterComponent to GetOrRegisterComponentID and updated all usages.
- Component registration codegen now uses a static class with [ModuleInitializer], no longer requires [EngineEntry].
- Improved JSON serialization: added string support, introduced Utf8JsonObjectScope/ArrayScope, and new extension methods for cleaner JSON writing.
- Removed [SkipLocalsInit] from Hierarchy and LocalToWorld.
- Fixed Entity.Invalid to use INVALID_ID for both fields.
- Minor cleanup: clarified comments, reorganized Ghost.Generator in solution, and disabled component serialization generator.
This commit is contained in:
2025-12-21 22:18:25 +09:00
parent 840cf7dd5a
commit 2881fda112
15 changed files with 90 additions and 137 deletions

View File

@@ -8,6 +8,7 @@ using System.Threading;
namespace Ghost.Generator
{
// TODO: this should be per assembly, not global
[Generator]
public class ComponentRegistrationGenerator : IIncrementalGenerator
{
@@ -21,20 +22,8 @@ namespace Ghost.Generator
.Where(symbol => symbol != null)
.Collect();
// 2. Pipeline: Find the ONE class with [EngineEntry]
var engineEntryClass = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is ClassDeclarationSyntax,
transform: GetEngineEntrySymbol)
.Where(symbol => symbol != null)
.Collect(); // Returns ImmutableArray<INamedTypeSymbol>
// 3. COMBINE: Pair the list of components with the list of entry classes
// The result 'source' in the callback will be a tuple: (ImmutableArray<Info>, ImmutableArray<Entry>)
var combinedProvider = componentCandidates.Combine(engineEntryClass);
// 4. Output: Generate the source using both pieces of data at once
context.RegisterSourceOutput(combinedProvider, GenerateRegistrationCode);
context.RegisterSourceOutput(componentCandidates, GenerateRegistrationCode);
}
// Extraction Logic for Components
@@ -63,76 +52,38 @@ namespace Ghost.Generator
return null;
}
// Extraction Logic for Engine Entry
private static INamedTypeSymbol GetEngineEntrySymbol(GeneratorSyntaxContext ctx, CancellationToken _)
{
var classSyntax = (ClassDeclarationSyntax)ctx.Node;
if (!(ctx.SemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol symbol))
{
return null;
}
// Check attributes
foreach (var attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Ghost.Engine.EngineEntryAttribute")
{
return symbol;
}
}
return null;
}
// The Generation Logic (Stateless)
private static void GenerateRegistrationCode(
SourceProductionContext context,
(ImmutableArray<INamedTypeSymbol> Components, ImmutableArray<INamedTypeSymbol> Entries) source)
private static void GenerateRegistrationCode(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> components)
{
var components = source.Components;
var entries = source.Entries;
// 1. Validation: Ensure we found exactly one [EngineEntry] class
if (entries.IsDefaultOrEmpty)
{
return;
}
// Pick the first one (if multiple exist, you might want to report a diagnostic error)
var targetClass = entries[0];
if (components.IsDefaultOrEmpty)
{
return;
}
// 2. Extract Namespace and Class Name directly from the symbol found in the pipeline
var targetNamespace = targetClass.ContainingNamespace.ToDisplayString();
var targetClassName = targetClass.Name;
var name = $"g_component_registration";
var sb = new StringBuilder();
sb.Append($@"
namespace {targetNamespace}
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static class {name}
{{
public partial class {targetClassName}
{{
private static void RegisterIComponentTypes()
{{");
[global::System.Runtime.CompilerServices.ModuleInitializer]
public static void RegisterIComponentTypes()
{{");
foreach (var symbol in components.Distinct(SymbolEqualityComparer.Default))
{
if (symbol is null) continue;
foreach (var symbol in components.Distinct(SymbolEqualityComparer.Default))
{
if (symbol is null) continue;
sb.Append($@"
global::Ghost.Entities.ComponentRegistry.GetOrRegisterComponent<{symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();");
}
sb.Append(@"
sb.Append($@"
global::Ghost.Entities.ComponentRegistry.GetOrRegisterComponentID<{symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();");
}
sb.Append(@"
}
}");
context.AddSource($"{targetClassName}.ComponentReg.gen.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
context.AddSource($"{name}.gen.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
}
}
}

View File

@@ -11,7 +11,6 @@ namespace Ghost.Generator
{
private string GetJsonWriteCall(ITypeSymbol type, string fieldName)
{
// 1. PRIMITIVES (Fastest)
switch (type.SpecialType)
{
case SpecialType.System_Byte:
@@ -32,26 +31,10 @@ namespace Ghost.Generator
return $@"writer.WriteBoolean(""{fieldName}"", value.{fieldName});";
case SpecialType.System_Char:
return $@"writer.WriteString(""{fieldName}"", [value.{fieldName}]);";
case SpecialType.System_String:
return $@"writer.WriteString(""{fieldName}"", value.{fieldName});";
}
// TODO: Add well-known types like float3, Vector3, etc.
#if false
// 2. KNOWN MATH TYPES (Optimized Arrays)
// Checking by name is simple and effective for standard math libs
if (type.Name == "float3" || type.Name == "Vector3")
{
return $@"writer.WritePropertyName(""{fieldName}"");
writer.WriteStartArray();
writer.WriteNumberValue(value.{fieldName}.x);
writer.WriteNumberValue(value.{fieldName}.y);
writer.WriteNumberValue(value.{fieldName}.z);
writer.WriteEndArray();";
}
#endif
// 3. FALLBACK: System.Text.Json (Reflection)
// If we don't know what it is, let the standard serializer handle it.
// This handles float4x4, List<T>, and other nested structs automatically.
return $@"writer.WritePropertyName(""{fieldName}""); global::System.Text.Json.JsonSerializer.Serialize(writer, value.{fieldName}, options);";
}
@@ -70,17 +53,14 @@ namespace Ghost.Generator
case SpecialType.System_UInt16: return "reader.GetUInt16()";
case SpecialType.System_UInt32: return "reader.GetUInt32()";
case SpecialType.System_UInt64: return "reader.GetUInt64()";
case SpecialType.System_IntPtr: return "reader.GetInt64()"; // Note: IntPtr size varies by platform
case SpecialType.System_UIntPtr: return "reader.GetUInt64()"; // Note: UIntPtr size varies by platform
// Note: the size of IntPtr and UIntPtr varies by platform, we use Int64/UInt64 to ensure compatibility
case SpecialType.System_IntPtr: return "reader.GetInt64()";
case SpecialType.System_UIntPtr: return "reader.GetUInt64()";
case SpecialType.System_Boolean: return "reader.GetBoolean()";
case SpecialType.System_Char: return "reader.GetString()[0]";
case SpecialType.System_String: return "reader.GetString()";
}
#if false
// For custom math types, you'd need to generate code to read the array back
if (type.Name == "float3") return "new float3(reader.GetSingle(), reader.GetSingle(), reader.GetSingle())"; // Simplified
#endif
return $"global::System.Text.Json.JsonSerializer.Deserialize<{type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(ref reader, options)";
}
@@ -125,6 +105,7 @@ namespace Ghost.Generator
// 2. The Main Template using $@
// Watch the double braces {{ }} and double quotes "" ""
var sourceCode = $@"// <auto-generated/>
#nullable enable
namespace {namespaceName}
{{
@@ -207,6 +188,8 @@ namespace {namespaceName}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
return;
var componentCandidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (syntaxNode, _) => syntaxNode is StructDeclarationSyntax,