switch root namespace name to NITLOAD

This commit is contained in:
Youmu Liu
2026-09-02 18:15:03 +08:00
parent 4218baad48
commit 7c7101ee48
502 changed files with 710 additions and 721 deletions
@@ -0,0 +1,429 @@
// -----------------------------------------------------------------------------
// file="GlFunctionFamilyAnalyzer"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using StgSharp.GenerateGL.Naming;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Analysis
{
/// <summary>
/// Builds inspectable function-family candidates without changing generation.
/// </summary>
internal static class GlFunctionFamilyAnalyzer
{
public static GlFunctionFamilyAnalysis Analyze(
GlRegistryModel registry
)
{
ArgumentNullException.ThrowIfNull(registry);
return Analyze(registry, registry.Commands);
}
public static GlFunctionFamilyAnalysis Analyze(
GlRegistryProjection projection
)
{
ArgumentNullException.ThrowIfNull(projection);
return Analyze(projection.Source, projection.Commands);
}
private static GlFunctionFamilyAnalysis Analyze(
GlRegistryModel registry,
IReadOnlyList<GlCommandDefinition> commands
)
{
Dictionary<string, IReadOnlyList<GlCommandDefinition>> commandsByName = commands
.GroupBy(value => value.Name, StringComparer.Ordinal)
.ToDictionary(
value => value.Key,
value => (IReadOnlyList<GlCommandDefinition>)Array.AsReadOnly(
value.ToArray()),
StringComparer.Ordinal);
Dictionary<string, GlFunctionNameShape> shapes =
GlFunctionNameFactorizer.Factor(registry, commands);
Dictionary<string, FamilyBuilder> builders = new Dictionary<string, FamilyBuilder>(
StringComparer.Ordinal);
List<GlFunctionRelation> relations = new List<GlFunctionRelation>();
foreach (GlCommandDefinition command in commands)
{
if (shapes.TryGetValue(command.Name, out GlFunctionNameShape? shape))
{
ReadBuilder(builders, shape.FamilyName).Add(command, shape);
}
}
AddRegistryRelations(commands, commandsByName, shapes, builders, relations);
AddSingleValuePointerRelations(commandsByName, shapes, builders, relations);
GlFunctionFamily[] families = builders.Values
.Where(value => value.Members.Count > 1 || value.Relations.Count != 0)
.Select(value => value.Build())
.OrderBy(value => value.Name, StringComparer.Ordinal)
.ToArray();
HashSet<string> classifiedNames = families
.SelectMany(value => value.Members)
.Select(value => value.Command.Name)
.ToHashSet(StringComparer.Ordinal);
GlCommandDefinition[] unclassified = commands
.Where(value => !classifiedNames.Contains(value.Name))
.OrderBy(value => value.Name, StringComparer.Ordinal)
.ToArray();
return new GlFunctionFamilyAnalysis(
Freeze(families),
Freeze(unclassified),
Freeze(relations));
}
private static void AddRegistryRelations(
IReadOnlyList<GlCommandDefinition> commands,
Dictionary<string, IReadOnlyList<GlCommandDefinition>> commandsByName,
Dictionary<string, GlFunctionNameShape> shapes,
IDictionary<string, FamilyBuilder> builders,
List<GlFunctionRelation> relations
)
{
foreach (GlCommandDefinition command in commands)
{
foreach (GlNamedReference reference in command.VectorEquivalents)
{
bool exists = commandsByName.TryGetValue(
reference.Name,
out IReadOnlyList<GlCommandDefinition>? targets);
GlFunctionRelation relation = new GlFunctionRelation(
command.Name,
reference.Name,
GlFunctionRelationKind.RegistryVectorEquivalent,
exists
? GlFunctionRelationState.Verified
: GlFunctionRelationState.MissingTarget,
exists
? "declared by gl.xml <vecequiv>"
: "gl.xml <vecequiv> target is not registered");
relations.Add(relation);
string familyName = ReadRelationFamilyName(
command.Name,
reference.Name,
shapes);
FamilyBuilder builder = ReadBuilder(builders, familyName);
builder.Add(
command,
shapes.TryGetValue(command.Name, out GlFunctionNameShape? sourceShape)
? sourceShape
: null);
builder.Relations.Add(relation);
if (!exists)
{
builder.Diagnostics.Add(
$"Missing vector-equivalent target '{reference.Name}'.");
continue;
}
foreach (GlCommandDefinition target in targets!)
{
builder.Add(
target,
shapes.TryGetValue(target.Name, out GlFunctionNameShape? targetShape)
? targetShape
: null);
}
}
}
}
private static void AddSingleValuePointerRelations(
Dictionary<string, IReadOnlyList<GlCommandDefinition>> commandsByName,
Dictionary<string, GlFunctionNameShape> shapes,
IDictionary<string, FamilyBuilder> builders,
List<GlFunctionRelation> relations
)
{
HashSet<string> existingPairs = relations
.Select(value => $"{value.SourceName}\0{value.TargetName}")
.ToHashSet(StringComparer.Ordinal);
foreach ((string commandName, GlFunctionNameShape pointerShape) in shapes)
{
if (!pointerShape.IsVectorForm ||
!commandsByName.TryGetValue(
commandName,
out IReadOnlyList<GlCommandDefinition>? pointerCommands))
{
continue;
}
string scalarName = commandName[..^1];
if (!shapes.TryGetValue(scalarName, out GlFunctionNameShape? scalarShape) ||
!commandsByName.TryGetValue(
scalarName,
out IReadOnlyList<GlCommandDefinition>? scalarCommands))
{
continue;
}
foreach (GlCommandDefinition pointer in pointerCommands)
{
if (pointer.Parameters.Count == 0 ||
!string.Equals(pointer.Parameters[^1].Length, "1", StringComparison.Ordinal))
{
continue;
}
foreach (GlCommandDefinition scalar in scalarCommands)
{
if (existingPairs.Contains($"{scalar.Name}\0{pointer.Name}"))
{
continue;
}
bool valid = IsSingleValuePointerPair(scalar, pointer, out string detail);
GlFunctionRelation relation = new GlFunctionRelation(
scalar.Name,
pointer.Name,
GlFunctionRelationKind.InferredSingleValuePointer,
valid
? GlFunctionRelationState.Verified
: GlFunctionRelationState.Rejected,
detail);
relations.Add(relation);
FamilyBuilder builder = ReadBuilder(builders, pointerShape.FamilyName);
builder.Add(scalar, scalarShape);
builder.Add(pointer, pointerShape);
builder.Relations.Add(relation);
if (!valid)
{
builder.Diagnostics.Add(
$"Rejected scalar/pointer pair {scalar.Name} -> {pointer.Name}: " +
detail);
}
}
}
}
}
private static bool IsSingleValuePointerPair(
GlCommandDefinition scalar,
GlCommandDefinition pointer,
out string detail
)
{
if (!string.Equals(
scalar.Return.TypeName,
pointer.Return.TypeName,
StringComparison.Ordinal) ||
scalar.Parameters.Count != pointer.Parameters.Count)
{
detail = "return type or parameter count differs";
return false;
}
int lastIndex = scalar.Parameters.Count - 1;
for (int index = 0; index < lastIndex; index++)
{
if (!HaveEquivalentParameterTypes(
scalar.Parameters[index],
pointer.Parameters[index]))
{
detail = $"parameter {index + 1} differs";
return false;
}
}
GlParameterDefinition scalarValue = scalar.Parameters[lastIndex];
GlParameterDefinition pointerValue = pointer.Parameters[lastIndex];
if (!string.Equals(
scalarValue.TypeName,
pointerValue.TypeName,
StringComparison.Ordinal) ||
scalarValue.DeclarationText.Contains('*', StringComparison.Ordinal) ||
!pointerValue.DeclarationText.Contains('*', StringComparison.Ordinal) ||
!pointerValue.DeclarationText.TrimStart().StartsWith(
"const ",
StringComparison.Ordinal) ||
!string.Equals(pointerValue.Length, "1", StringComparison.Ordinal))
{
detail = "last parameter is not a matching T / const T* len=1 pair";
return false;
}
detail = "verified T / const T* len=1 pair";
return true;
}
private static bool HaveEquivalentParameterTypes(
GlParameterDefinition left,
GlParameterDefinition right
)
{
return string.Equals(left.TypeName, right.TypeName, StringComparison.Ordinal) &&
left.DeclarationText.Contains('*', StringComparison.Ordinal) ==
right.DeclarationText.Contains('*', StringComparison.Ordinal);
}
private static string ReadRelationFamilyName(
string sourceName,
string targetName,
Dictionary<string, GlFunctionNameShape> shapes
)
{
if (shapes.TryGetValue(sourceName, out GlFunctionNameShape? sourceShape))
{
return sourceShape.FamilyName;
}
if (shapes.TryGetValue(targetName, out GlFunctionNameShape? targetShape))
{
return targetShape.FamilyName;
}
string source = RemoveGlPrefix(sourceName);
string target = RemoveGlPrefix(targetName);
if (target.EndsWith('v') &&
string.Equals(source, target[..^1], StringComparison.Ordinal))
{
return CSharpIdentifierPolicy.NormalizeMemberName(source);
}
int length = 0;
int maximum = Math.Min(source.Length, target.Length);
while (length < maximum && source[length] == target[length])
{
length++;
}
string familyName = length == 0 ? source : source[..length].TrimEnd('_');
return CSharpIdentifierPolicy.NormalizeMemberName(familyName);
}
private static string RemoveGlPrefix(
string name
)
{
return name.StartsWith("gl", StringComparison.Ordinal) && name.Length > 2
? name[2..]
: name;
}
private static FamilyBuilder ReadBuilder(
IDictionary<string, FamilyBuilder> builders,
string familyName
)
{
if (!builders.TryGetValue(familyName, out FamilyBuilder? builder))
{
builder = new FamilyBuilder(familyName);
builders.Add(familyName, builder);
}
return builder;
}
private static ReadOnlyCollection<T> Freeze<T>(
IEnumerable<T> values
)
{
return Array.AsReadOnly(values.ToArray());
}
private sealed class FamilyBuilder
{
private readonly Dictionary<string, GlFunctionFamilyMember> _members = new Dictionary<string, GlFunctionFamilyMember>(
StringComparer.Ordinal);
public FamilyBuilder(
string name
)
{
Name = name;
}
public string Name { get; }
public Dictionary<string, GlFunctionFamilyMember>.ValueCollection Members =>
_members.Values;
public List<GlFunctionRelation> Relations { get; } = new List<GlFunctionRelation>();
public List<string> Diagnostics { get; } = new List<string>();
public void Add(
GlCommandDefinition command,
GlFunctionNameShape? shape
)
{
_members.TryAdd(
command.Name,
new GlFunctionFamilyMember(command, shape));
}
public GlFunctionFamily Build()
{
GlFunctionFamilyMember[] members = _members.Values
.OrderBy(value => value.Command.Name, StringComparer.Ordinal)
.ToArray();
string[] diagnostics = Diagnostics
.Distinct(StringComparer.Ordinal)
.OrderBy(value => value, StringComparer.Ordinal)
.ToArray();
return new GlFunctionFamily(
Name,
Freeze(members),
Freeze(Relations.Distinct().ToArray()),
Freeze(diagnostics),
ReadShapeNamePolicy(members));
}
private static GlFunctionShapeNamePolicy ReadShapeNamePolicy(
IReadOnlyCollection<GlFunctionFamilyMember> members
)
{
GlFunctionFamilyMember[] scalarMembers = members
.Where(
value => value.NameShape is
{ IsVectorForm: false, Shape: not null })
.ToArray();
if (scalarMembers.Select(value => value.NameShape!.Shape)
.Distinct(StringComparer.Ordinal)
.Count() < 2)
{
return GlFunctionShapeNamePolicy.NotApplicable;
}
bool collision = scalarMembers
.GroupBy(ReadNativeSignature, StringComparer.Ordinal)
.Any(group => group.Select(value => value.NameShape!.Shape)
.Distinct(StringComparer.Ordinal)
.Count() > 1);
return collision
? GlFunctionShapeNamePolicy.MustRetain
: GlFunctionShapeNamePolicy.CanRemove;
}
private static string ReadNativeSignature(
GlFunctionFamilyMember member
)
{
GlCommandDefinition command = member.Command;
return $"{command.Return.TypeName}:" + string.Join(
",",
command.Parameters.Select(
value => $"{value.TypeName}:{value.DeclarationText.Contains('*', StringComparison.Ordinal)}"));
}
}
}
}
@@ -0,0 +1,68 @@
// -----------------------------------------------------------------------------
// file="GlFunctionFamilyModel"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Analysis
{
internal sealed record GlFunctionNameShape(
string FamilyName,
string? Shape,
int? Columns,
int? Rows,
string TypeSuffix,
bool IsVectorForm
);
internal sealed record GlFunctionFamilyMember(
GlCommandDefinition Command,
GlFunctionNameShape? NameShape
);
internal sealed record GlFunctionRelation(
string SourceName,
string TargetName,
GlFunctionRelationKind Kind,
GlFunctionRelationState State,
string Detail
);
internal sealed record GlFunctionFamily(
string Name,
IReadOnlyList<GlFunctionFamilyMember> Members,
IReadOnlyList<GlFunctionRelation> Relations,
IReadOnlyList<string> Diagnostics,
GlFunctionShapeNamePolicy ShapeNamePolicy
);
internal sealed record GlFunctionFamilyAnalysis(
IReadOnlyList<GlFunctionFamily> Families,
IReadOnlyList<GlCommandDefinition> UnclassifiedCommands,
IReadOnlyList<GlFunctionRelation> Relations
);
internal enum GlFunctionRelationKind
{
RegistryVectorEquivalent,
InferredSingleValuePointer,
}
internal enum GlFunctionRelationState
{
Verified,
MissingTarget,
Rejected,
}
internal enum GlFunctionShapeNamePolicy
{
NotApplicable,
CanRemove,
MustRetain,
}
}
@@ -0,0 +1,472 @@
// -----------------------------------------------------------------------------
// file="GlFunctionNameFactorizer"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using StgSharp.GenerateGL.Naming;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Analysis
{
/// <summary>
/// Factors command-name alternatives by their common prefix and interprets only the
/// remaining suffix as overload axes.
/// </summary>
internal static class GlFunctionNameFactorizer
{
private static readonly string[] _typeSuffixes =
{
"ui64",
"i64",
"ui",
"ub",
"us",
"b",
"s",
"i",
"f",
"d",
"h",
"x",
};
public static Dictionary<string, GlFunctionNameShape> Factor(
GlRegistryModel registry,
IReadOnlyList<GlCommandDefinition> commands
)
{
ArgumentNullException.ThrowIfNull(registry);
ArgumentNullException.ThrowIfNull(commands);
string[] vendorSuffixes = ReadVendorSuffixes(registry);
NameEntry[] entries = commands
.Select(
command => new NameEntry(
command,
command.Name,
ReadCoreName(command.Name, vendorSuffixes)))
.ToArray();
TrieNode root = new TrieNode(string.Empty);
for (int index = 0; index < entries.Length; index++)
{
Add(root, entries[index].CoreName, index);
}
List<FactorCandidate>[] candidatesByEntry = Enumerable.Range(0, entries.Length)
.Select(_ => new List<FactorCandidate>())
.ToArray();
CollectCandidates(root, entries, candidatesByEntry);
Dictionary<string, GlFunctionNameShape> result = new Dictionary<string, GlFunctionNameShape>(
StringComparer.Ordinal);
for (int index = 0; index < entries.Length; index++)
{
FactorCandidate? candidate = candidatesByEntry[index]
.OrderByDescending(value => value.DistinctVariantCount)
.ThenByDescending(value => value.MemberCount)
.ThenBy(value => value.FamilyName.Length)
.FirstOrDefault();
if (candidate is null ||
!candidate.Shapes.TryGetValue(index, out GlFunctionNameShape? shape))
{
continue;
}
result.TryAdd(entries[index].CommandName, shape);
}
AddFixedVectorPairs(result, commands);
return result;
}
private static void AddFixedVectorPairs(
Dictionary<string, GlFunctionNameShape> shapes,
IReadOnlyList<GlCommandDefinition> commands
)
{
Dictionary<string, GlCommandDefinition> commandsByName = commands.ToDictionary(
value => value.Name,
StringComparer.Ordinal);
foreach (GlCommandDefinition vector in commands)
{
if (!vector.Name.EndsWith('v') ||
shapes.ContainsKey(vector.Name) ||
vector.Parameters.Count == 0 ||
!int.TryParse(vector.Parameters[^1].Length, out int componentCount) ||
componentCount < 2)
{
continue;
}
string scalarName = vector.Name[..^1];
if (shapes.ContainsKey(scalarName) ||
!commandsByName.TryGetValue(
scalarName,
out GlCommandDefinition? scalar) ||
!IsFixedVectorPair(scalar, vector, componentCount))
{
continue;
}
string familyName = CSharpIdentifierPolicy.NormalizeMemberName(
scalarName.StartsWith("gl", StringComparison.Ordinal) &&
scalarName.Length > 2
? scalarName[2..]
: scalarName);
shapes.Add(
scalarName,
new GlFunctionNameShape(
familyName,
null,
null,
null,
string.Empty,
false));
shapes.Add(
vector.Name,
new GlFunctionNameShape(
familyName,
null,
null,
null,
string.Empty,
true));
}
}
private static bool IsFixedVectorPair(
GlCommandDefinition scalar,
GlCommandDefinition vector,
int componentCount
)
{
if (!string.Equals(
scalar.Return.TypeName,
vector.Return.TypeName,
StringComparison.Ordinal) ||
scalar.Parameters.Count != vector.Parameters.Count - 1 + componentCount)
{
return false;
}
int prefixCount = vector.Parameters.Count - 1;
for (int index = 0; index < prefixCount; index++)
{
if (!HaveEquivalentScalarTypes(
scalar.Parameters[index].TypeName,
vector.Parameters[index].TypeName) ||
scalar.Parameters[index].DeclarationText.Contains('*', StringComparison.Ordinal) !=
vector.Parameters[index].DeclarationText.Contains('*', StringComparison.Ordinal))
{
return false;
}
}
GlParameterDefinition vectorValue = vector.Parameters[^1];
if (!vectorValue.DeclarationText.Contains('*', StringComparison.Ordinal))
{
return false;
}
for (int index = prefixCount; index < scalar.Parameters.Count; index++)
{
if (!HaveEquivalentScalarTypes(
scalar.Parameters[index].TypeName,
vectorValue.TypeName) ||
scalar.Parameters[index].DeclarationText.Contains('*', StringComparison.Ordinal))
{
return false;
}
}
return true;
}
private static bool HaveEquivalentScalarTypes(
string? left,
string? right
)
{
if (string.Equals(left, right, StringComparison.Ordinal))
{
return true;
}
return left is "GLint" or "GLsizei" &&
right is "GLint" or "GLsizei";
}
private static List<int> CollectCandidates(
TrieNode node,
IReadOnlyList<NameEntry> entries,
IReadOnlyList<List<FactorCandidate>> candidatesByEntry
)
{
List<int> descendants = new List<int>(node.Terminals);
foreach (TrieNode child in node.Children.Values)
{
descendants.AddRange(CollectCandidates(child, entries, candidatesByEntry));
}
if (node.Prefix.Length == 0 || descendants.Count < 2)
{
return descendants;
}
Dictionary<int, GlFunctionNameShape> shapes = new Dictionary<int, GlFunctionNameShape>();
foreach (int index in descendants)
{
string coreName = entries[index].CoreName;
if (coreName.Length <= node.Prefix.Length ||
!TryParseVariantSuffix(
coreName[node.Prefix.Length..],
node.Prefix,
entries[index].Command,
out GlFunctionNameShape? shape))
{
continue;
}
shapes.Add(index, shape!);
}
int distinctVariantCount = shapes.Keys
.Select(index => entries[index].CoreName)
.Distinct(StringComparer.Ordinal)
.Count();
if (distinctVariantCount < 2)
{
return descendants;
}
FactorCandidate candidate = new FactorCandidate(
node.Prefix,
shapes,
distinctVariantCount);
foreach (int index in shapes.Keys)
{
candidatesByEntry[index].Add(candidate);
}
return descendants;
}
private static bool TryParseVariantSuffix(
string suffix,
string familyName,
GlCommandDefinition command,
out GlFunctionNameShape? shape
)
{
bool isVectorForm = suffix.EndsWith('v');
if (isVectorForm)
{
suffix = suffix[..^1];
}
string? typeSuffix = null;
foreach (string candidate in _typeSuffixes)
{
if (suffix.EndsWith(candidate, StringComparison.Ordinal))
{
typeSuffix = candidate;
suffix = suffix[..^candidate.Length];
break;
}
}
if (typeSuffix is null ||
!IsTypeSuffixConsistent(command, typeSuffix) ||
!TryParseShape(
suffix,
out string? shapeText,
out int? columns,
out int? rows))
{
shape = null;
return false;
}
shape = new GlFunctionNameShape(
CSharpIdentifierPolicy.NormalizeMemberName(familyName),
shapeText,
columns,
rows,
typeSuffix,
isVectorForm);
return true;
}
private static bool IsTypeSuffixConsistent(
GlCommandDefinition command,
string typeSuffix
)
{
string? glType = typeSuffix switch
{
"b" => "GLbyte",
"ub" => "GLubyte",
"s" => "GLshort",
"us" => "GLushort",
"i" => "GLint",
"ui" => "GLuint",
"i64" => "GLint64",
"ui64" => "GLuint64",
"f" => "GLfloat",
"d" => "GLdouble",
"h" => "GLhalf",
"x" => "GLfixed",
_ => null,
};
if (glType is null)
{
return false;
}
return string.Equals(command.Return.TypeName, glType, StringComparison.Ordinal) ||
command.Parameters.Any(
value => string.Equals(
value.TypeName,
glType,
StringComparison.Ordinal));
}
private static bool TryParseShape(
string value,
out string? shape,
out int? columns,
out int? rows
)
{
if (value.Length == 0)
{
shape = null;
columns = null;
rows = null;
return true;
}
if (value.Length == 1 && value[0] is >= '1' and <= '4')
{
shape = value;
columns = value[0] - '0';
rows = 1;
return true;
}
if (value.Length == 3 &&
value[0] is >= '2' and <= '4' &&
value[1] == 'x' &&
value[2] is >= '2' and <= '4')
{
shape = value;
columns = value[0] - '0';
rows = value[2] - '0';
return true;
}
shape = null;
columns = null;
rows = null;
return false;
}
private static void Add(
TrieNode root,
string value,
int entryIndex
)
{
TrieNode current = root;
foreach (char character in value)
{
if (!current.Children.TryGetValue(character, out TrieNode? child))
{
child = new TrieNode(current.Prefix + character);
current.Children.Add(character, child);
}
current = child;
}
current.Terminals.Add(entryIndex);
}
private static string ReadCoreName(
string commandName,
IReadOnlyList<string> vendorSuffixes
)
{
string name = commandName.StartsWith("gl", StringComparison.Ordinal) &&
commandName.Length > 2
? commandName[2..]
: commandName;
foreach (string vendorSuffix in vendorSuffixes)
{
if (name.Length > vendorSuffix.Length && name.EndsWith(
vendorSuffix,
StringComparison.Ordinal))
{
return name[..^vendorSuffix.Length];
}
}
return name;
}
private static string[] ReadVendorSuffixes(
GlRegistryModel registry
)
{
return registry.Extensions
.Select(value => value.Name.Split('_', StringSplitOptions.RemoveEmptyEntries))
.Where(value => value.Length > 2 && value[0] == "GL")
.Select(value => value[1])
.Where(value => value.All(character => char.IsUpper(character) || char.IsDigit(character)))
.Distinct(StringComparer.Ordinal)
.OrderByDescending(value => value.Length)
.ThenBy(value => value, StringComparer.Ordinal)
.ToArray();
}
private sealed record NameEntry(
GlCommandDefinition Command,
string CommandName,
string CoreName
);
private sealed record FactorCandidate(
string FamilyName,
IReadOnlyDictionary<int, GlFunctionNameShape> Shapes,
int DistinctVariantCount
)
{
public int MemberCount => Shapes.Count;
}
private sealed class TrieNode
{
public TrieNode(
string prefix
)
{
Prefix = prefix;
}
public string Prefix { get; }
public Dictionary<char, TrieNode> Children { get; } = new Dictionary<char, TrieNode>();
public List<int> Terminals { get; } = new List<int>();
}
}
}
@@ -0,0 +1,556 @@
// -----------------------------------------------------------------------------
// file="FunctionFamilyCli"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StgSharp.GenerateGL.Analysis;
namespace StgSharp.GenerateGL
{
/// <summary>
/// Presents the function-family analysis without affecting generation.
/// </summary>
internal static class FunctionFamilyCli
{
private const int ShowAllResults = int.MaxValue;
private const int QueryErrorExitCode = 1;
private const int SuccessExitCode = 0;
private const int UsageErrorExitCode = 2;
public static int Execute(
GlFunctionFamilyAnalysis analysis,
IReadOnlyList<string> arguments
)
{
ArgumentNullException.ThrowIfNull(analysis);
ArgumentNullException.ThrowIfNull(arguments);
if (arguments.Count == 0)
{
WriteHelp();
return SuccessExitCode;
}
string command = arguments[0].ToUpperInvariant();
string[] queryArguments = arguments.Skip(1).ToArray();
return command switch
{
"SUMMARY" => QuerySummary(analysis, queryArguments),
"LIST" or "LS" => QueryList(analysis, queryArguments),
"SHOW" or "INFO" => QueryShow(analysis, queryArguments),
"RELATIONS" or "PAIRS" => QueryRelations(analysis, queryArguments),
"UNRESOLVED" or "UNCLASSIFIED" => QueryUnclassified(
analysis,
queryArguments),
"CONFLICTS" or "CONSTRAINTS" => QueryConstraints(
analysis,
queryArguments),
"HELP" or "?" => WriteHelpResult(queryArguments),
_ => WriteUsageError($"Unknown family command '{arguments[0]}'."),
};
}
public static void WriteHelp()
{
Console.WriteLine("Function-family analysis commands:");
Console.WriteLine(" families summary");
Console.WriteLine(" families list [filter] [--showcount <n>]");
Console.WriteLine(" families show <family>");
Console.WriteLine(" families relations [filter] [--showcount <n>]");
Console.WriteLine(" families unresolved [filter] [--showcount <n>]");
Console.WriteLine(" families conflicts [filter] [--showcount <n>]");
}
private static int QuerySummary(
GlFunctionFamilyAnalysis analysis,
string[] arguments
)
{
if (arguments.Length != 0)
{
return WriteUsageError("families summary does not accept arguments.");
}
Console.WriteLine("Function-family analysis");
WriteProperty("families", analysis.Families.Count);
WriteProperty(
"classified commands",
analysis.Families.SelectMany(value => value.Members)
.Select(value => value.Command.Name)
.Distinct(StringComparer.Ordinal)
.Count());
WriteProperty("unclassified commands", analysis.UnclassifiedCommands.Count);
WriteProperty("relations", analysis.Relations.Count);
WriteProperty(
"registry vector relations",
analysis.Relations.Count(
value => value.Kind == GlFunctionRelationKind.RegistryVectorEquivalent));
WriteProperty(
"inferred len=1 relations",
analysis.Relations.Count(
value => value.Kind == GlFunctionRelationKind.InferredSingleValuePointer));
WriteProperty(
"verified relations",
analysis.Relations.Count(
value => value.State == GlFunctionRelationState.Verified));
WriteProperty(
"relations requiring review",
analysis.Relations.Count(
value => value.State != GlFunctionRelationState.Verified));
WriteProperty(
"shape suffix must remain",
analysis.Families.Count(
value => value.ShapeNamePolicy == GlFunctionShapeNamePolicy.MustRetain));
return SuccessExitCode;
}
private static int QueryList(
GlFunctionFamilyAnalysis analysis,
IReadOnlyList<string> arguments
)
{
if (!TryReadFilterOptions(
arguments,
out string? filter,
out int limit,
out string? error))
{
return WriteUsageError(error);
}
GlFunctionFamily[] families = FilterFamilies(analysis, filter).ToArray();
if (families.Length == 0)
{
Console.Error.WriteLine(
filter is null
? "No function families were recognized."
: $"No function families contain '{filter}'.");
return QueryErrorExitCode;
}
int displayedCount = Math.Min(families.Length, limit);
for (int index = 0; index < displayedCount; index++)
{
GlFunctionFamily family = families[index];
Console.WriteLine($"[{index + 1}] {family.Name}");
WriteProperty("members", family.Members.Count);
WriteProperty(
"shapes",
JoinDistinct(
family.Members.Select(value => value.NameShape?.Shape)));
WriteProperty(
"types",
JoinDistinct(
family.Members.Select(value => value.NameShape?.TypeSuffix)));
WriteProperty(
"forms",
JoinDistinct(
family.Members.Where(value => value.NameShape is not null)
.Select(
value => value.NameShape!.IsVectorForm
? "pointer"
: "scalar")));
WriteProperty("relations", family.Relations.Count);
WriteProperty("shape name", ReadShapePolicy(family.ShapeNamePolicy));
}
WriteTruncation(families.Length, displayedCount);
return SuccessExitCode;
}
private static int QueryShow(
GlFunctionFamilyAnalysis analysis,
string[] arguments
)
{
if (arguments.Length != 1)
{
return WriteUsageError("families show requires one exact family name.");
}
GlFunctionFamily? family = analysis.Families.FirstOrDefault(
value => string.Equals(
value.Name,
arguments[0],
StringComparison.OrdinalIgnoreCase));
if (family is null)
{
Console.Error.WriteLine($"No function family named '{arguments[0]}'.");
Console.Error.WriteLine($"Try: families list {arguments[0]}");
return QueryErrorExitCode;
}
Console.WriteLine($"function family: {family.Name}");
WriteProperty("members", family.Members.Count);
WriteProperty("shape name", ReadShapePolicy(family.ShapeNamePolicy));
for (int index = 0; index < family.Members.Count; index++)
{
GlFunctionFamilyMember member = family.Members[index];
Console.WriteLine($" [{index + 1}] {member.Command.Name}");
if (member.NameShape is not null)
{
if (member.NameShape.Shape is not null)
{
WriteIndentedProperty("shape", member.NameShape.Shape, 8);
}
WriteIndentedProperty(
"type suffix",
string.IsNullOrWhiteSpace(member.NameShape.TypeSuffix)
? "-"
: member.NameShape.TypeSuffix,
8);
WriteIndentedProperty(
"form",
member.NameShape.IsVectorForm ? "pointer" : "scalar",
8);
}
WriteIndentedProperty(
"parameters",
ReadParameterList(member.Command),
8);
}
WriteRelations(family.Relations);
if (family.Diagnostics.Count != 0)
{
Console.WriteLine(" diagnostics:");
foreach (string diagnostic in family.Diagnostics)
{
Console.WriteLine($" - {diagnostic}");
}
}
return SuccessExitCode;
}
private static int QueryRelations(
GlFunctionFamilyAnalysis analysis,
IReadOnlyList<string> arguments
)
{
if (!TryReadFilterOptions(
arguments,
out string? filter,
out int limit,
out string? error))
{
return WriteUsageError(error);
}
GlFunctionRelation[] relations = analysis.Relations
.Where(
value => filter is null ||
value.SourceName.Contains(
filter,
StringComparison.OrdinalIgnoreCase) ||
value.TargetName.Contains(
filter,
StringComparison.OrdinalIgnoreCase))
.OrderBy(value => value.SourceName, StringComparer.Ordinal)
.ThenBy(value => value.TargetName, StringComparer.Ordinal)
.ToArray();
if (relations.Length == 0)
{
Console.Error.WriteLine(
filter is null
? "No function relations were recognized."
: $"No function relations contain '{filter}'.");
return QueryErrorExitCode;
}
int displayedCount = Math.Min(relations.Length, limit);
for (int index = 0; index < displayedCount; index++)
{
WriteRelation(index + 1, relations[index]);
}
WriteTruncation(relations.Length, displayedCount);
return SuccessExitCode;
}
private static int QueryUnclassified(
GlFunctionFamilyAnalysis analysis,
IReadOnlyList<string> arguments
)
{
if (!TryReadFilterOptions(
arguments,
out string? filter,
out int limit,
out string? error))
{
return WriteUsageError(error);
}
string[] commands = analysis.UnclassifiedCommands
.Select(value => value.Name)
.Where(
value => filter is null || value.Contains(
filter,
StringComparison.OrdinalIgnoreCase))
.ToArray();
if (commands.Length == 0)
{
Console.Error.WriteLine(
filter is null
? "Every command belongs to a candidate family."
: $"No unclassified commands contain '{filter}'.");
return QueryErrorExitCode;
}
int displayedCount = Math.Min(commands.Length, limit);
for (int index = 0; index < displayedCount; index++)
{
Console.WriteLine($"[{index + 1}] {commands[index]}");
}
WriteTruncation(commands.Length, displayedCount);
return SuccessExitCode;
}
private static int QueryConstraints(
GlFunctionFamilyAnalysis analysis,
IReadOnlyList<string> arguments
)
{
if (!TryReadFilterOptions(
arguments,
out string? filter,
out int limit,
out string? error))
{
return WriteUsageError(error);
}
GlFunctionFamily[] families = FilterFamilies(analysis, filter)
.Where(
value => value.ShapeNamePolicy == GlFunctionShapeNamePolicy.MustRetain ||
value.Diagnostics.Count != 0 ||
value.Relations.Any(
relation => relation.State != GlFunctionRelationState.Verified))
.ToArray();
if (families.Length == 0)
{
Console.Error.WriteLine(
filter is null
? "No family constraints require review."
: $"No constrained families contain '{filter}'.");
return QueryErrorExitCode;
}
int displayedCount = Math.Min(families.Length, limit);
for (int index = 0; index < displayedCount; index++)
{
GlFunctionFamily family = families[index];
Console.WriteLine($"[{index + 1}] {family.Name}");
if (family.ShapeNamePolicy == GlFunctionShapeNamePolicy.MustRetain)
{
WriteProperty(
"constraint",
"shape suffix must remain because normalized signatures collide");
}
foreach (string diagnostic in family.Diagnostics)
{
WriteProperty("diagnostic", diagnostic);
}
}
WriteTruncation(families.Length, displayedCount);
return SuccessExitCode;
}
private static IEnumerable<GlFunctionFamily> FilterFamilies(
GlFunctionFamilyAnalysis analysis,
string? filter
)
{
return analysis.Families.Where(
value => filter is null || value.Name.Contains(
filter,
StringComparison.OrdinalIgnoreCase));
}
private static bool TryReadFilterOptions(
IReadOnlyList<string> arguments,
out string? filter,
out int limit,
out string? error
)
{
filter = null;
limit = ShowAllResults;
int index = 0;
if (index < arguments.Count && !arguments[index].StartsWith(
"--",
StringComparison.Ordinal))
{
filter = arguments[index++];
}
while (index < arguments.Count)
{
string argument = arguments[index++];
if ((argument == "--showcount" || argument == "--limit") &&
index < arguments.Count && int.TryParse(
arguments[index++],
NumberStyles.None,
CultureInfo.InvariantCulture,
out limit) && limit > 0)
{
continue;
}
error = $"Unknown or invalid family option '{argument}'.";
return false;
}
error = null;
return true;
}
private static string ReadParameterList(
Registry.GlCommandDefinition command
)
{
return string.Join(
", ",
command.Parameters.Select(value => value.DeclarationText));
}
private static void WriteRelations(
IReadOnlyList<GlFunctionRelation> relations
)
{
if (relations.Count == 0)
{
return;
}
Console.WriteLine(" relations:");
for (int index = 0; index < relations.Count; index++)
{
WriteRelation(index + 1, relations[index], 4);
}
}
private static void WriteRelation(
int index,
GlFunctionRelation relation,
int indentation = 0
)
{
string prefix = new string(' ', indentation);
Console.WriteLine(
$"{prefix}[{index}] {relation.SourceName} -> {relation.TargetName}");
WriteIndentedProperty(
"source",
relation.Kind == GlFunctionRelationKind.RegistryVectorEquivalent
? "gl.xml vecequiv"
: "inferred len=1 pair",
indentation + 4);
WriteIndentedProperty(
"state",
ReadRelationState(relation.State),
indentation + 4);
WriteIndentedProperty("detail", relation.Detail, indentation + 4);
}
private static string ReadShapePolicy(
GlFunctionShapeNamePolicy policy
)
{
return policy switch
{
GlFunctionShapeNamePolicy.NotApplicable => "not applicable",
GlFunctionShapeNamePolicy.CanRemove => "may be removed",
GlFunctionShapeNamePolicy.MustRetain => "must remain",
_ => throw new ArgumentOutOfRangeException(nameof(policy)),
};
}
private static string ReadRelationState(
GlFunctionRelationState state
)
{
return state switch
{
GlFunctionRelationState.Verified => "verified",
GlFunctionRelationState.MissingTarget => "missing target",
GlFunctionRelationState.Rejected => "rejected",
_ => throw new ArgumentOutOfRangeException(nameof(state)),
};
}
private static string JoinDistinct(
IEnumerable<string?> values
)
{
string[] distinct = values.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(value => value!)
.Distinct(StringComparer.Ordinal)
.OrderBy(value => value, StringComparer.Ordinal)
.ToArray();
return distinct.Length == 0 ? "-" : string.Join(", ", distinct);
}
private static int WriteHelpResult(
string[] arguments
)
{
if (arguments.Length != 0)
{
return WriteUsageError("families help does not accept arguments.");
}
WriteHelp();
return SuccessExitCode;
}
private static int WriteUsageError(
string? error
)
{
Console.Error.WriteLine($"error: {error}");
return UsageErrorExitCode;
}
private static void WriteProperty(
string name,
object value
)
{
WriteIndentedProperty(name, value, 2);
}
private static void WriteIndentedProperty(
string name,
object value,
int indentation
)
{
Console.WriteLine($"{new string(' ', indentation)}{name}: {value}");
}
private static void WriteTruncation(
int totalCount,
int displayedCount
)
{
if (displayedCount != totalCount)
{
Console.WriteLine($"... {totalCount - displayedCount} more result(s)");
}
}
}
}
+319
View File
@@ -0,0 +1,319 @@
// -----------------------------------------------------------------------------
// file="GenerateCli"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using StgSharp.GenerateGL.Generation;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL
{
/// <summary>
/// Owns the command-line boundary of the future code generation pipeline.
/// </summary>
internal static class GenerateCli
{
private const int QueryErrorExitCode = 1;
private const int SuccessExitCode = 0;
private const int UsageErrorExitCode = 2;
private const string GeneratedFileMarker = "// <auto-generated>";
public static int Generate(
GlRegistryProjection projection,
IReadOnlyList<string> arguments,
out GeneratedFileSet? generatedFiles
)
{
ArgumentNullException.ThrowIfNull(projection);
ArgumentNullException.ThrowIfNull(arguments);
generatedFiles = null;
if (arguments.Count == 1 && arguments[0] is "--help" or "-h")
{
WriteHelp();
return SuccessExitCode;
}
if (arguments.Count == 0 || arguments.Count > 2 ||
!TryReadTarget(arguments[0], out GlGenerationTarget target))
{
Console.Error.WriteLine(
"error: generate requires api, enum, or all and accepts one optional output directory.");
Console.Error.WriteLine("Run 'generate --help' for command information.");
return UsageErrorExitCode;
}
string? outputDirectory = arguments.Count == 2 ? arguments[1] : null;
string? resolvedOutputDirectory = null;
try
{
generatedFiles = OpenGlGenerator.Generate(projection, target);
if (outputDirectory is not null)
{
resolvedOutputDirectory = WriteGeneratedFiles(
generatedFiles,
outputDirectory);
}
}
catch (InvalidDataException exception)
{
Console.Error.WriteLine($"error: {exception.Message}");
return QueryErrorExitCode;
}
catch (InvalidOperationException exception)
{
Console.Error.WriteLine($"error: {exception.Message}");
return QueryErrorExitCode;
}
catch (UnauthorizedAccessException exception)
{
Console.Error.WriteLine($"error: {exception.Message}");
return QueryErrorExitCode;
}
catch (IOException exception)
{
Console.Error.WriteLine($"error: {exception.Message}");
return QueryErrorExitCode;
}
catch (ArgumentException exception)
{
Console.Error.WriteLine($"error: {exception.Message}");
return QueryErrorExitCode;
}
catch (NotSupportedException exception)
{
Console.Error.WriteLine($"error: {exception.Message}");
return QueryErrorExitCode;
}
Console.WriteLine(
$"Generated {generatedFiles.Files.Count} file(s) in memory.");
Console.WriteLine("Use 'list files' and 'preview <filename>' to inspect them.");
Console.WriteLine(
resolvedOutputDirectory is null
? "No files were written to disk."
: $"Wrote {generatedFiles.Files.Count} file(s) to " +
$"'{resolvedOutputDirectory}'.");
return SuccessExitCode;
}
private static bool TryReadTarget(
string value,
out GlGenerationTarget target
)
{
target = value.ToUpperInvariant() switch
{
"API" => GlGenerationTarget.Api,
"ENUM" or "ENUMS" => GlGenerationTarget.Enum,
"ALL" => GlGenerationTarget.All,
_ => GlGenerationTarget.None,
};
return target != GlGenerationTarget.None;
}
private static string WriteGeneratedFiles(
GeneratedFileSet files,
string outputDirectory
)
{
ArgumentNullException.ThrowIfNull(files);
ArgumentException.ThrowIfNullOrWhiteSpace(outputDirectory);
string directory = Path.TrimEndingDirectorySeparator(
Path.GetFullPath(outputDirectory));
if (!Directory.Exists(directory))
{
throw new DirectoryNotFoundException(
$"Output directory '{directory}' does not exist.");
}
StringComparison pathComparison = OperatingSystem.IsWindows()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
List<StagedGeneratedFile> stagedFiles = new List<StagedGeneratedFile>(
files.Files.Count);
try
{
foreach (GeneratedFile file in files.Files)
{
string fileName = Path.GetFileName(file.Name);
if (!string.Equals(fileName, file.Name, StringComparison.Ordinal) ||
string.IsNullOrWhiteSpace(fileName))
{
throw new InvalidDataException(
$"Generated file name '{file.Name}' is not a plain file name.");
}
string targetPath = Path.GetFullPath(Path.Combine(directory, fileName));
if (!string.Equals(
Path.GetDirectoryName(targetPath),
directory,
pathComparison))
{
throw new InvalidDataException(
$"Generated file '{file.Name}' escapes the output directory.");
}
EnsureTargetCanBeReplaced(targetPath);
string temporaryPath = Path.Combine(
directory,
$".{fileName}.{Guid.NewGuid():N}.tmp");
using (FileStream stream = new FileStream(
temporaryPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None))
using (StreamWriter writer = new StreamWriter(
stream,
new UTF8Encoding(false)))
{
writer.Write(file.Content);
}
stagedFiles.Add(new StagedGeneratedFile(temporaryPath, targetPath));
}
foreach (StagedGeneratedFile file in stagedFiles)
{
File.Move(file.TemporaryPath, file.TargetPath, true);
}
}
finally
{
foreach (StagedGeneratedFile file in stagedFiles)
{
if (File.Exists(file.TemporaryPath))
{
File.Delete(file.TemporaryPath);
}
}
}
return directory;
}
private static void EnsureTargetCanBeReplaced(
string targetPath
)
{
if (!File.Exists(targetPath))
{
return;
}
string? firstLine = File.ReadLines(targetPath).FirstOrDefault();
if (!string.Equals(firstLine, GeneratedFileMarker, StringComparison.Ordinal))
{
throw new IOException(
$"Refusing to overwrite non-generated file '{targetPath}'.");
}
}
public static int ListFiles(
GeneratedFileSet files,
IReadOnlyList<string> arguments
)
{
ArgumentNullException.ThrowIfNull(files);
ArgumentNullException.ThrowIfNull(arguments);
if (arguments.Count != 0)
{
Console.Error.WriteLine("error: list files does not accept arguments.");
return UsageErrorExitCode;
}
if (files.Files.Count == 0)
{
Console.Error.WriteLine("No generated files are held in this session.");
Console.Error.WriteLine("Run 'generate api', 'generate enum', or 'generate all' first.");
return QueryErrorExitCode;
}
Console.WriteLine("Generated files held in the current session");
for (int index = 0; index < files.Files.Count; index++)
{
GeneratedFile file = files.Files[index];
Console.WriteLine(
$" [{index + 1}] {file.Name} ({file.Content.Length} characters)");
}
return SuccessExitCode;
}
public static int Preview(
GeneratedFileSet files,
IReadOnlyList<string> arguments
)
{
ArgumentNullException.ThrowIfNull(files);
ArgumentNullException.ThrowIfNull(arguments);
if (arguments.Count != 1)
{
Console.Error.WriteLine("error: preview requires one generated file name.");
return UsageErrorExitCode;
}
if (!files.TryGetFile(arguments[0], out GeneratedFile file))
{
Console.Error.WriteLine(
$"No generated file named '{arguments[0]}' is held in this session.");
if (files.Files.Count == 0)
{
Console.Error.WriteLine(
"Run 'generate api', 'generate enum', or 'generate all' first.");
}
else
{
Console.Error.WriteLine(
$"Available files: {string.Join(", ", files.Files.Select(value => value.Name))}");
}
return QueryErrorExitCode;
}
Console.Write(file.Content);
return SuccessExitCode;
}
public static void WriteHelp()
{
Console.WriteLine("Generate C# bindings from the loaded OpenGL registry.");
Console.WriteLine();
Console.WriteLine("Usage:");
Console.WriteLine(" generate api [output-directory]");
Console.WriteLine(" generate enum [output-directory]");
Console.WriteLine(" generate all [output-directory]");
Console.WriteLine(" generate --help");
Console.WriteLine();
Console.WriteLine("Targets:");
Console.WriteLine(" api Generate function tables and managed forwarding APIs.");
Console.WriteLine(" enum Generate glConst and strongly typed enum groups.");
Console.WriteLine(" all Generate both API and enum files.");
Console.WriteLine();
Console.WriteLine("Output:");
Console.WriteLine(" Without a directory, output is retained in memory only.");
Console.WriteLine(" API output references enum output; use all for a clean directory.");
Console.WriteLine(" The output directory must already exist and be writable.");
Console.WriteLine(" Existing files are replaced only when marked as generated.");
Console.WriteLine();
Console.WriteLine("Inspection:");
Console.WriteLine(" list files");
Console.WriteLine(" preview <filename>");
}
private sealed record StagedGeneratedFile(
string TemporaryPath,
string TargetPath
);
}
}
@@ -0,0 +1,64 @@
// -----------------------------------------------------------------------------
// file="CSharpAbiTypePolicy"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Defines C ABI primitives and external Khronos platform types that cannot
/// be inferred from gl.xml.
/// </summary>
internal static class CSharpAbiTypePolicy
{
private static readonly Dictionary<string, string> _types = new Dictionary<string, string>(
StringComparer.Ordinal)
{
["void"] = "void",
["char"] = "byte",
["signed char"] = "sbyte",
["unsigned char"] = "byte",
["short"] = "short",
["short int"] = "short",
["signed short"] = "short",
["signed short int"] = "short",
["unsigned short"] = "ushort",
["unsigned short int"] = "ushort",
["int"] = "int",
["signed"] = "int",
["signed int"] = "int",
["unsigned"] = "uint",
["unsigned int"] = "uint",
["float"] = "float",
["double"] = "double",
["khronos_int8_t"] = "sbyte",
["khronos_uint8_t"] = "byte",
["khronos_int16_t"] = "short",
["khronos_uint16_t"] = "ushort",
["khronos_int32_t"] = "int",
["khronos_uint32_t"] = "uint",
["khronos_int64_t"] = "long",
["khronos_uint64_t"] = "ulong",
["khronos_intptr_t"] = "nint",
["khronos_uintptr_t"] = "nuint",
["khronos_ssize_t"] = "nint",
["khronos_usize_t"] = "nuint",
["khronos_float_t"] = "float",
};
public static bool TryResolve(
string typeName,
out string equivalentType
)
{
return _types.TryGetValue(typeName, out equivalentType!);
}
}
}
@@ -0,0 +1,63 @@
// -----------------------------------------------------------------------------
// file="GeneratedFileSet"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace StgSharp.GenerateGL.Generation
{
internal sealed record GeneratedFile(
string Name,
string Content
);
internal sealed class GeneratedFileSet
{
public static GeneratedFileSet Empty { get; } = new GeneratedFileSet(
Array.Empty<GeneratedFile>());
private readonly ReadOnlyDictionary<string, GeneratedFile> _filesByName;
public GeneratedFileSet(
IEnumerable<GeneratedFile> files
)
{
ArgumentNullException.ThrowIfNull(files);
ReadOnlyCollection<GeneratedFile> values = Array.AsReadOnly(files.ToArray());
Dictionary<string, GeneratedFile> index = new Dictionary<string, GeneratedFile>(
StringComparer.OrdinalIgnoreCase);
foreach (GeneratedFile file in values)
{
ArgumentException.ThrowIfNullOrWhiteSpace(file.Name);
if (!index.TryAdd(file.Name, file))
{
throw new InvalidOperationException(
$"Generated file name '{file.Name}' is duplicated.");
}
}
Files = values;
_filesByName = new ReadOnlyDictionary<string, GeneratedFile>(index);
}
public IReadOnlyList<GeneratedFile> Files { get; }
public bool TryGetFile(
string name,
out GeneratedFile file
)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
return _filesByName.TryGetValue(name, out file!);
}
}
}
@@ -0,0 +1,336 @@
// -----------------------------------------------------------------------------
// file="GlEnumProjector"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using StgSharp.GenerateGL.Naming;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Projects registry GLenum and GLbitfield parameter groups into C# enum types.
/// </summary>
internal static class GlEnumProjector
{
private static readonly string[] _coreExtensionSuffixes =
{
"ARB",
"EXT",
"KHR",
"NV",
};
private static readonly ReadOnlyDictionary<string, string> _managedTypeNameOverrides =
new ReadOnlyDictionary<string, string>(
new Dictionary<string, string>(StringComparer.Ordinal)
{
["Buffer"] = "ClearBuffer",
});
public static IReadOnlyList<ManagedEnumDefinition> Project(
GlRegistryProjection projection
)
{
ArgumentNullException.ThrowIfNull(projection);
Dictionary<string, bool> referencedGroups = ReadReferencedGroups(projection.Commands);
Dictionary<string, GlEnumDefinition> selectedEnums = projection.Enums
.Where(value => value.Value is not null)
.GroupBy(value => value.Name, StringComparer.Ordinal)
.ToDictionary(
value => value.Key,
value => value.First(),
StringComparer.Ordinal);
Dictionary<string, List<GlEnumDefinition>> membersByGroup =
new Dictionary<string, List<GlEnumDefinition>>(StringComparer.Ordinal);
foreach (GlEnumDefinition value in selectedEnums.Values)
{
foreach (string group in SplitGroups(value.Group))
{
if (!referencedGroups.ContainsKey(group))
{
continue;
}
if (!membersByGroup.TryGetValue(group, out List<GlEnumDefinition>? members))
{
members = new List<GlEnumDefinition>();
membersByGroup.Add(group, members);
}
members.Add(value);
}
}
Dictionary<string, EnumBuilder> enumsByManagedName =
new Dictionary<string, EnumBuilder>(StringComparer.Ordinal);
foreach ((string nativeGroup, bool isFlags) in referencedGroups.OrderBy(
value => value.Key,
StringComparer.Ordinal))
{
if (!membersByGroup.TryGetValue(nativeGroup, out List<GlEnumDefinition>? members) ||
members.Count == 0)
{
continue;
}
string managedName = ReadManagedTypeName(nativeGroup);
if (!enumsByManagedName.TryGetValue(managedName, out EnumBuilder? builder))
{
builder = new EnumBuilder(managedName);
enumsByManagedName.Add(managedName, builder);
}
builder.AddGroup(nativeGroup, isFlags, members);
}
return Array.AsReadOnly(
enumsByManagedName.Values
.OrderBy(value => value.Name, StringComparer.Ordinal)
.Select(value => value.Build())
.ToArray());
}
private static Dictionary<string, bool> ReadReferencedGroups(
IReadOnlyList<GlCommandDefinition> commands
)
{
Dictionary<string, bool> result = new Dictionary<string, bool>(
StringComparer.Ordinal);
foreach (GlCommandDefinition command in commands)
{
AddReferencedGroups(
result,
command.Return.Group,
command.Return.TypeName);
foreach (GlParameterDefinition parameter in command.Parameters)
{
AddReferencedGroups(result, parameter.Group, parameter.TypeName);
}
}
return result;
}
private static void AddReferencedGroups(
Dictionary<string, bool> result,
string? groupNames,
string? nativeType
)
{
if (groupNames is null || nativeType is not ("GLenum" or "GLbitfield"))
{
return;
}
bool isFlags = string.Equals(nativeType, "GLbitfield", StringComparison.Ordinal);
foreach (string group in SplitGroups(groupNames))
{
result[group] = result.TryGetValue(group, out bool previous)
? previous || isFlags
: isFlags;
}
}
private static IEnumerable<string> SplitGroups(
string? value
)
{
if (string.IsNullOrWhiteSpace(value))
{
yield break;
}
foreach (string item in value.Split(','))
{
string group = item.Trim();
if (group.Length != 0)
{
yield return group;
}
}
}
private static string ReadManagedTypeName(
string nativeGroup
)
{
if (_managedTypeNameOverrides.TryGetValue(nativeGroup, out string? overrideName))
{
return overrideName;
}
string value = nativeGroup;
foreach (string suffix in _coreExtensionSuffixes)
{
if (value.Length > suffix.Length && value.EndsWith(
suffix,
StringComparison.Ordinal))
{
value = value[..^suffix.Length];
break;
}
}
return CSharpIdentifierPolicy.NormalizeMemberName(SanitizeIdentifier(value));
}
private static string ReadManagedMemberName(
string nativeName
)
{
string value = nativeName.StartsWith("GL_", StringComparison.Ordinal)
? nativeName[3..]
: nativeName;
StringBuilder builder = new StringBuilder(value.Length);
foreach (string component in value.Split('_', StringSplitOptions.RemoveEmptyEntries))
{
if (component.Length == 0)
{
continue;
}
if (char.IsLetter(component[0]))
{
builder.Append(char.ToUpperInvariant(component[0]));
if (component.Length > 1)
{
foreach (char character in component.AsSpan(1))
{
builder.Append(ToAsciiLowerInvariant(character));
}
}
}
else
{
builder.Append(component);
}
}
string result = builder.Length == 0 ? "Value" : builder.ToString();
return CSharpIdentifierPolicy.NormalizeMemberName(SanitizeIdentifier(result));
}
private static char ToAsciiLowerInvariant(
char value
)
{
return value is >= 'A' and <= 'Z' ? (char)(value + ('a' - 'A')) : value;
}
private static string SanitizeIdentifier(
string value
)
{
StringBuilder builder = new StringBuilder(value.Length + 1);
if (value.Length == 0 || !char.IsLetter(value[0]) && value[0] != '_')
{
builder.Append('_');
}
foreach (char character in value)
{
builder.Append(char.IsLetterOrDigit(character) || character == '_'
? character
: '_');
}
return builder.ToString();
}
private sealed class EnumBuilder
{
private readonly Dictionary<string, ManagedEnumMemberDefinition> _members =
new Dictionary<string, ManagedEnumMemberDefinition>(StringComparer.Ordinal);
private readonly HashSet<string> _nativeGroups = new HashSet<string>(
StringComparer.Ordinal);
public EnumBuilder(
string name
)
{
Name = name;
}
public bool IsFlags { get; private set; }
public string Name { get; }
public void AddGroup(
string nativeGroup,
bool isFlags,
IEnumerable<GlEnumDefinition> members
)
{
_nativeGroups.Add(nativeGroup);
IsFlags |= isFlags;
foreach (GlEnumDefinition value in members)
{
string name = ReadManagedMemberName(value.Name);
if (string.Equals(name, Name, StringComparison.Ordinal))
{
name = $"{name}Value";
}
if (_members.TryGetValue(name, out ManagedEnumMemberDefinition? previous))
{
if (!string.Equals(
previous.NativeValue.Value,
value.Value,
StringComparison.Ordinal))
{
throw new InvalidDataException(
$"Enum member '{Name}.{name}' maps to conflicting native values " +
$"'{previous.NativeValue.Name}' and '{value.Name}'.");
}
continue;
}
_members.Add(name, new ManagedEnumMemberDefinition(name, value));
}
}
public ManagedEnumDefinition Build()
{
List<ManagedEnumMemberDefinition> members = _members.Values
.OrderBy(value => value.Name, StringComparer.Ordinal)
.ToList();
if (IsFlags && members.All(value => value.NativeValue.Value != "0" &&
value.NativeValue.Value != "0x0" &&
value.NativeValue.Value != "0x00000000"))
{
members.Insert(
0,
new ManagedEnumMemberDefinition(
"None",
new GlEnumDefinition(
"GL_NONE",
"0",
null,
null,
string.Join(",", _nativeGroups),
null,
"Synthetic zero value for a generated flags enum.",
members[0].NativeValue.Element)));
}
return new ManagedEnumDefinition(
Name,
IsFlags,
Array.AsReadOnly(
_nativeGroups.OrderBy(value => value, StringComparer.Ordinal).ToArray()),
Array.AsReadOnly(members.ToArray()));
}
}
}
}
@@ -0,0 +1,122 @@
// -----------------------------------------------------------------------------
// file="GlEnumTypeMap"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Resolves unambiguous scalar GLenum and GLbitfield groups to generated C# types.
/// </summary>
internal sealed class GlEnumTypeMap
{
private readonly Dictionary<string, string> _typesByNativeGroup;
public GlEnumTypeMap(
IEnumerable<ManagedEnumDefinition> definitions
)
{
ArgumentNullException.ThrowIfNull(definitions);
_typesByNativeGroup = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (ManagedEnumDefinition definition in definitions)
{
foreach (string nativeGroup in definition.NativeGroups)
{
if (_typesByNativeGroup.TryGetValue(nativeGroup, out string? previous) &&
!string.Equals(previous, definition.Name, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Native enum group '{nativeGroup}' maps to both " +
$"'{previous}' and '{definition.Name}'.");
}
_typesByNativeGroup[nativeGroup] = definition.Name;
}
}
}
public bool TryReadParameterType(
GlParameterDefinition parameter,
out string managedType
)
{
ArgumentNullException.ThrowIfNull(parameter);
if (!IsEnumDeclaration(parameter.TypeName, parameter.Group))
{
managedType = string.Empty;
return false;
}
return TryReadGroupType(parameter.Group!, out managedType);
}
public bool TryReadReturnType(
GlReturnDefinition value,
out string managedType
)
{
ArgumentNullException.ThrowIfNull(value);
if (!IsEnumDeclaration(value.TypeName, value.Group))
{
managedType = string.Empty;
return false;
}
return TryReadGroupType(value.Group!, out managedType);
}
private static bool IsEnumDeclaration(
string? nativeType,
string? group
)
{
return group is not null &&
nativeType is "GLenum" or "GLbitfield";
}
private bool TryReadGroupType(
string groups,
out string managedType
)
{
string[] nativeGroups = groups.Split(',')
.Select(value => value.Trim())
.Where(value => value.Length != 0)
.ToArray();
if (nativeGroups.Length == 0)
{
managedType = string.Empty;
return false;
}
HashSet<string> candidates = new HashSet<string>(StringComparer.Ordinal);
foreach (string nativeGroup in nativeGroups)
{
if (!_typesByNativeGroup.TryGetValue(nativeGroup, out string? candidate))
{
managedType = string.Empty;
return false;
}
candidates.Add(candidate);
}
if (candidates.Count == 1)
{
managedType = candidates.First();
return true;
}
managedType = string.Empty;
return false;
}
}
}
@@ -0,0 +1,242 @@
// -----------------------------------------------------------------------------
// file="GlEquivalentTypeMapper"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Resolves OpenGL typedef declarations recursively to equivalent C# ABI types.
/// </summary>
internal sealed class GlEquivalentTypeMapper
{
private readonly Dictionary<string, string> _resolvedTypes = new Dictionary<string, string>(
StringComparer.Ordinal);
private readonly GlRegistryModel _registry;
public GlEquivalentTypeMapper(
GlRegistryModel registry
)
{
ArgumentNullException.ThrowIfNull(registry);
_registry = registry;
}
public string ReadReturnType(
GlCommandDefinition command
)
{
ArgumentNullException.ThrowIfNull(command);
return ReadDeclarationType(
command.Return.TypeName,
command.Return.DeclarationText,
command.Name);
}
public string ReadParameterType(
GlParameterDefinition parameter
)
{
ArgumentNullException.ThrowIfNull(parameter);
return ReadDeclarationType(
parameter.TypeName,
parameter.DeclarationText,
parameter.Name);
}
private string ReadDeclarationType(
string? typeName,
string declaration,
string identifier
)
{
if (typeName is null)
{
return ResolveTypeExpression(RemoveIdentifier(declaration, identifier));
}
string equivalentType = ResolveNamedType(
typeName,
new HashSet<string>(StringComparer.Ordinal));
int pointerDepth = declaration.Count(value => value == '*');
if (declaration.Contains('[', StringComparison.Ordinal))
{
pointerDepth++;
}
return equivalentType + new string('*', pointerDepth);
}
private string ResolveNamedType(
string typeName,
HashSet<string> resolving
)
{
if (CSharpAbiTypePolicy.TryResolve(typeName, out string abiType))
{
return abiType;
}
if (_resolvedTypes.TryGetValue(typeName, out string? resolvedType))
{
return resolvedType;
}
if (!resolving.Add(typeName))
{
throw new InvalidDataException(
$"OpenGL type alias cycle contains '{typeName}'.");
}
if (!_registry.TypesByName.TryGetValue(
typeName,
out IReadOnlyList<GlTypeDefinition>? definitions) ||
definitions.Count != 1)
{
throw new InvalidDataException(
$"OpenGL type '{typeName}' does not have one registry definition.");
}
GlTypeDefinition definition = definitions[0];
string declaration = NormalizeWhitespace(definition.DeclarationText);
string value;
if (TryReadCallbackType(typeName, declaration, resolving, out string callbackType))
{
value = callbackType;
}
else
{
MatchCollection aliases = Regex.Matches(
declaration,
$@"typedef\s+(?<target>.+?)\s*\b{Regex.Escape(typeName)}\b\s*;",
RegexOptions.CultureInvariant);
if (aliases.Count == 1)
{
value = ResolveTypeExpression(
aliases[0].Groups["target"].Value,
resolving);
}
else if (declaration == $"{typeName};" || declaration.StartsWith(
"struct ",
StringComparison.Ordinal))
{
value = "void";
}
else if (aliases.Count > 1)
{
throw new InvalidDataException(
$"OpenGL type '{typeName}' has platform-dependent typedefs and " +
"requires an explicit target policy.");
}
else
{
throw new InvalidDataException(
$"Cannot read typedef declaration '{definition.DeclarationText}'.");
}
}
resolving.Remove(typeName);
_resolvedTypes.Add(typeName, value);
return value;
}
private bool TryReadCallbackType(
string typeName,
string declaration,
HashSet<string> resolving,
out string type
)
{
Match match = Regex.Match(
declaration,
$@"^typedef\s+(?<return>.+?)\s*\(\s*\*\s*{Regex.Escape(typeName)}\s*\)" +
@"\s*\((?<parameters>.*)\)\s*;$",
RegexOptions.CultureInvariant);
if (!match.Success)
{
type = string.Empty;
return false;
}
List<string> types = new List<string>();
string parameters = match.Groups["parameters"].Value.Trim();
if (parameters.Length != 0 && parameters != "void")
{
foreach (string parameter in parameters.Split(','))
{
types.Add(ResolveTypeExpression(RemoveLastIdentifier(parameter), resolving));
}
}
types.Add(ResolveTypeExpression(match.Groups["return"].Value, resolving));
type = $"delegate* unmanaged<{string.Join(", ", types)}>";
return true;
}
private string ResolveTypeExpression(
string expression,
HashSet<string>? resolving = null
)
{
resolving ??= new HashSet<string>(StringComparer.Ordinal);
string value = Regex.Replace(
NormalizeWhitespace(expression),
@"\b(?:const|volatile|restrict)\b",
string.Empty,
RegexOptions.CultureInvariant).Trim();
int pointerDepth = value.Count(character => character == '*');
value = value.Replace("*", string.Empty, StringComparison.Ordinal).Trim();
if (value.StartsWith("struct ", StringComparison.Ordinal))
{
value = "void";
}
string equivalentType = ResolveNamedType(value, resolving);
return equivalentType + new string('*', pointerDepth);
}
private static string RemoveIdentifier(
string declaration,
string identifier
)
{
int index = declaration.LastIndexOf(identifier, StringComparison.Ordinal);
return index < 0
? declaration
: declaration.Remove(index, identifier.Length);
}
private static string RemoveLastIdentifier(
string declaration
)
{
Match match = Regex.Match(
declaration.Trim(),
@"^(?<type>.+?)(?<name>[A-Za-z_][A-Za-z0-9_]*)$",
RegexOptions.CultureInvariant);
return match.Success ? match.Groups["type"].Value.Trim() : declaration.Trim();
}
private static string NormalizeWhitespace(
string value
)
{
return Regex.Replace(
value,
@"\s+",
" ",
RegexOptions.CultureInvariant).Trim();
}
}
}
@@ -0,0 +1,20 @@
// -----------------------------------------------------------------------------
// file="GlGenerationTarget"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
namespace StgSharp.GenerateGL.Generation
{
[Flags]
internal enum GlGenerationTarget
{
None = 0,
Api = 1,
Enum = 2,
All = Api | Enum,
}
}
@@ -0,0 +1,111 @@
// -----------------------------------------------------------------------------
// file="GlGetValueApiFilter"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Replaces desktop state query pointer families with typed Span overloads.
/// </summary>
internal sealed class GlGetValueApiFilter : IGlApiFilter
{
private static readonly Regex _pattern = new Regex(
@"^glGet(?<type>Boolean|Double|Float|Integer64|Integer)(?<indexed>i_)?v$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public IReadOnlyList<ManagedMethodDefinition> Apply(
GlCommandDefinition command,
IReadOnlyList<ManagedMethodDefinition> methods,
GlEquivalentTypeMapper typeMapper
)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(methods);
ArgumentNullException.ThrowIfNull(typeMapper);
Match match = _pattern.Match(command.Name);
if (!match.Success)
{
return methods;
}
bool isIndexed = match.Groups["indexed"].Success;
int prefixCount = isIndexed ? 2 : 1;
if (typeMapper.ReadReturnType(command) != "void" ||
command.Parameters.Count != prefixCount + 1)
{
throw new InvalidOperationException(
$"State query '{command.Name}' does not match its recognized shape.");
}
GlParameterDefinition output = command.Parameters[^1];
string elementType = ReadElementType(match.Groups["type"].Value);
string expectedLength = $"COMPSIZE({command.Parameters[0].Name})";
if (output.DeclarationText.Contains("const", StringComparison.Ordinal) ||
typeMapper.ReadParameterType(output) != $"{elementType}*" ||
!string.Equals(output.Length, expectedLength, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"State query '{command.Name}' has inconsistent output metadata.");
}
List<ManagedParameterDefinition> parameters = new List<ManagedParameterDefinition>();
List<ManagedArgument> arguments = new List<ManagedArgument>();
for (int index = 0; index < prefixCount; index++)
{
GlParameterDefinition source = command.Parameters[index];
parameters.Add(
new ManagedParameterDefinition(
typeMapper.ReadParameterType(source),
source.Name,
source));
arguments.Add(new ParameterArgument(index));
}
parameters.Add(
new ManagedParameterDefinition(
$"Span<{elementType}>",
"values",
output));
return Array.AsReadOnly(
new[]
{
new ManagedMethodDefinition(
"GetValue",
"void",
Array.AsReadOnly(parameters.ToArray()),
command,
new QuerySpanInvocation(
Array.AsReadOnly(arguments.ToArray()),
parameters.Count - 1,
elementType)),
});
}
private static string ReadElementType(
string name
)
{
return name switch
{
"Boolean" => "byte",
"Double" => "double",
"Float" => "float",
"Integer" => "int",
"Integer64" => "long",
_ => throw new InvalidOperationException(
$"Unsupported state query component type '{name}'."),
};
}
}
}
@@ -0,0 +1,169 @@
// -----------------------------------------------------------------------------
// file="GlHandleApiFilter"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Replaces registry object-class GLuint values with the managed GlHandle type.
/// </summary>
internal sealed class GlHandleApiFilter : IGlApiFilter
{
public IReadOnlyList<ManagedMethodDefinition> Apply(
GlCommandDefinition command,
IReadOnlyList<ManagedMethodDefinition> methods,
GlEquivalentTypeMapper typeMapper
)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(methods);
ArgumentNullException.ThrowIfNull(typeMapper);
bool wrapsReturn = string.Equals(
command.Return.TypeName,
"GLuint",
StringComparison.Ordinal) &&
command.Return.ObjectClass is not null;
return Array.AsReadOnly(
methods.Select(
method => ConvertMethod(method, wrapsReturn)).ToArray());
}
private static ManagedMethodDefinition ConvertMethod(
ManagedMethodDefinition method,
bool wrapsReturn
)
{
ManagedParameterDefinition[] converted = method.Parameters.Select(
ConvertParameter).ToArray();
if (method.Invocation is not DirectInvocation direct ||
!converted.Any(value => value.IsHandleSpan))
{
return method with
{
ReturnType = wrapsReturn ? "GlHandle" : method.ReturnType,
Parameters = Array.AsReadOnly(converted),
WrapHandleReturn = wrapsReturn,
};
}
Dictionary<string, int> spanByLengthName = new Dictionary<string, int>(
StringComparer.Ordinal);
for (int index = 0; index < converted.Length; index++)
{
ManagedParameterDefinition parameter = converted[index];
string? length = parameter.NativeParameter?.Length;
if (parameter.IsHandleSpan &&
length is not null &&
IsIdentifier(length))
{
spanByLengthName.TryAdd(length, index);
}
}
Dictionary<int, int> oldToNew = new Dictionary<int, int>();
List<ManagedParameterDefinition> parameters = new List<ManagedParameterDefinition>();
for (int oldIndex = 0; oldIndex < converted.Length; oldIndex++)
{
ManagedParameterDefinition parameter = converted[oldIndex];
if (spanByLengthName.ContainsKey(parameter.Name))
{
continue;
}
oldToNew.Add(oldIndex, parameters.Count);
parameters.Add(parameter);
}
List<ManagedArgument> arguments = new List<ManagedArgument>(direct.Arguments.Count);
foreach (ManagedArgument argument in direct.Arguments)
{
if (argument is not ParameterArgument sourceArgument)
{
arguments.Add(argument);
continue;
}
ManagedParameterDefinition sourceParameter = converted[sourceArgument.ParameterIndex];
if (spanByLengthName.TryGetValue(
sourceParameter.Name,
out int spanOldIndex))
{
arguments.Add(new SpanLengthArgument(oldToNew[spanOldIndex]));
}
else
{
arguments.Add(
new ParameterArgument(oldToNew[sourceArgument.ParameterIndex]));
}
}
return method with
{
ReturnType = wrapsReturn ? "GlHandle" : method.ReturnType,
Parameters = Array.AsReadOnly(parameters.ToArray()),
Invocation = new DirectInvocation(Array.AsReadOnly(arguments.ToArray())),
WrapHandleReturn = wrapsReturn,
};
}
private static ManagedParameterDefinition ConvertParameter(
ManagedParameterDefinition parameter
)
{
GlParameterDefinition? source = parameter.NativeParameter;
if (source is null ||
source.ObjectClass is null ||
!string.Equals(source.TypeName, "GLuint", StringComparison.Ordinal))
{
return parameter;
}
int pointerDepth = source.DeclarationText.Count(value => value == '*');
bool isSpan = pointerDepth == 1 && source.Length is not null;
string type;
if (isSpan)
{
type = source.DeclarationText.Contains("const", StringComparison.Ordinal)
? "ReadOnlySpan<GlHandle>"
: "Span<GlHandle>";
}
else
{
type = pointerDepth == 0
? "GlHandle"
: $"GlHandle{new string('*', pointerDepth)}";
}
return parameter with
{
Type = type,
IsHandle = true,
IsHandleSpan = isSpan,
};
}
private static bool IsIdentifier(
string value
)
{
if (value.Length == 0 ||
!char.IsLetter(value[0]) && value[0] != '_')
{
return false;
}
return value.Skip(1).All(character => char.IsLetterOrDigit(character) || character == '_');
}
}
}
@@ -0,0 +1,44 @@
// -----------------------------------------------------------------------------
// file="GlManagedApiSuppressionFilter"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Omits raw managed forwards for commands that have safer handwritten public APIs.
/// </summary>
internal sealed class GlManagedApiSuppressionFilter : IGlApiFilter
{
private static readonly HashSet<string> _suppressedCommands = new HashSet<string>(
new[]
{
"glCreateShader",
"glShaderSource",
},
StringComparer.Ordinal);
public IReadOnlyList<ManagedMethodDefinition> Apply(
GlCommandDefinition command,
IReadOnlyList<ManagedMethodDefinition> methods,
GlEquivalentTypeMapper typeMapper
)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(methods);
ArgumentNullException.ThrowIfNull(typeMapper);
return _suppressedCommands.Contains(command.Name)
? Array.Empty<ManagedMethodDefinition>()
: methods;
}
}
}
@@ -0,0 +1,400 @@
// -----------------------------------------------------------------------------
// file="GlOverloadApiFilter"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text.RegularExpressions;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Replaces verified C-style command families with managed overloads.
/// </summary>
internal sealed class GlOverloadApiFilter : IGlApiFilter
{
private static readonly Regex _matrixUniformPattern = new Regex(
@"^gl(?<family>ProgramUniform|Uniform)Matrix(?<columns>[2-4])" +
@"(?:x(?<rows>[2-4]))?(?<type>f|d)v$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex _uniformPattern = new Regex(
@"^gl(?<family>ProgramUniform|Uniform)(?<width>[1-4])" +
@"(?<type>ui|f|i|d)(?<vector>v?)$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex _vertexAttribPattern = new Regex(
@"^glVertexAttrib(?<width>[1-4])(?<type>d|f|s)(?<vector>v?)$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex _redundantVertexAttributeVectorPattern = new Regex(
@"^glVertexAttrib(?:I[1-4](?:i|ui)|L[1-4]d|P[1-4]ui|4Nub)v$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly string[] _components = { "X", "Y", "Z", "W" };
public IReadOnlyList<ManagedMethodDefinition> Apply(
GlCommandDefinition command,
IReadOnlyList<ManagedMethodDefinition> methods,
GlEquivalentTypeMapper typeMapper
)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(methods);
ArgumentNullException.ThrowIfNull(typeMapper);
if (_redundantVertexAttributeVectorPattern.IsMatch(command.Name))
{
return Array.Empty<ManagedMethodDefinition>();
}
Match matrixMatch = _matrixUniformPattern.Match(command.Name);
if (matrixMatch.Success)
{
return RewriteMatrixUniform(command, matrixMatch, typeMapper);
}
Match uniformMatch = _uniformPattern.Match(command.Name);
if (uniformMatch.Success)
{
return RewriteUniform(command, uniformMatch, typeMapper);
}
Match vertexAttribMatch = _vertexAttribPattern.Match(command.Name);
if (vertexAttribMatch.Success)
{
return vertexAttribMatch.Groups["vector"].Value.Length == 0
? Array.AsReadOnly(
new[]
{
methods[0] with { Name = "VertexAttrib" },
})
: Array.Empty<ManagedMethodDefinition>();
}
return methods;
}
private static ReadOnlyCollection<ManagedMethodDefinition> RewriteUniform(
GlCommandDefinition command,
Match match,
GlEquivalentTypeMapper typeMapper
)
{
int width = int.Parse(
match.Groups["width"].Value,
CultureInfo.InvariantCulture);
string componentType = ReadComponentType(match.Groups["type"].Value);
bool isVector = match.Groups["vector"].Value.Length != 0;
int prefixCount = match.Groups["family"].Value == "ProgramUniform" ? 2 : 1;
string methodName = match.Groups["family"].Value;
ValidateUniformSignature(
command,
width,
componentType,
prefixCount,
isVector,
typeMapper);
List<ManagedParameterDefinition> prefix = ReadNativeParameters(
command,
prefixCount,
typeMapper);
List<ManagedArgument> prefixArguments = ReadParameterArguments(prefix.Count);
if (isVector)
{
if (width is 2 or 3)
{
return Array.AsReadOnly(Array.Empty<ManagedMethodDefinition>());
}
GlParameterDefinition valueParameter = command.Parameters[^1];
string spanElementType = width == 1
? componentType
: $"Vec{width}<{componentType}>";
List<ManagedParameterDefinition> parameters = new List<ManagedParameterDefinition>(prefix)
{
new ManagedParameterDefinition(
$"ReadOnlySpan<{spanElementType}>",
"values",
valueParameter),
};
ManagedInvocation invocation = width == 1
? new PinnedSpanInvocation(
prefixArguments,
parameters.Count - 1,
componentType)
: new PinnedVectorSpanInvocation(
prefixArguments,
parameters.Count - 1,
componentType,
spanElementType);
return Array.AsReadOnly(
new[]
{
new ManagedMethodDefinition(
methodName,
"void",
Array.AsReadOnly(parameters.ToArray()),
command,
invocation),
});
}
List<ManagedMethodDefinition> methods = new List<ManagedMethodDefinition>();
List<ManagedParameterDefinition> scalarParameters = ReadNativeParameters(
command,
command.Parameters.Count,
typeMapper);
methods.Add(
new ManagedMethodDefinition(
methodName,
"void",
Array.AsReadOnly(scalarParameters.ToArray()),
command,
new DirectInvocation(ReadParameterArguments(scalarParameters.Count))));
if (width > 1)
{
List<ManagedParameterDefinition> vectorParameters = new List<ManagedParameterDefinition>(prefix)
{
new ManagedParameterDefinition(
$"Vec{width}<{componentType}>",
"value"),
};
List<ManagedArgument> vectorArguments = new List<ManagedArgument>(prefixArguments);
for (int index = 0; index < width; index++)
{
vectorArguments.Add(
new VectorComponentArgument(
vectorParameters.Count - 1,
_components[index]));
}
methods.Add(
new ManagedMethodDefinition(
methodName,
"void",
Array.AsReadOnly(vectorParameters.ToArray()),
command,
new DirectInvocation(Array.AsReadOnly(vectorArguments.ToArray()))));
}
return Array.AsReadOnly(methods.ToArray());
}
private static IReadOnlyList<ManagedMethodDefinition> RewriteMatrixUniform(
GlCommandDefinition command,
Match match,
GlEquivalentTypeMapper typeMapper
)
{
int columns = int.Parse(
match.Groups["columns"].Value,
CultureInfo.InvariantCulture);
int rows = match.Groups["rows"].Success
? int.Parse(
match.Groups["rows"].Value,
CultureInfo.InvariantCulture)
: columns;
string componentType = ReadComponentType(match.Groups["type"].Value);
int prefixCount = match.Groups["family"].Value == "ProgramUniform" ? 2 : 1;
string methodName = match.Groups["family"].Value;
ValidateMatrixSignature(
command,
columns,
rows,
componentType,
prefixCount,
typeMapper);
if (columns != 4 || rows != 4 || componentType != "float")
{
return Array.Empty<ManagedMethodDefinition>();
}
List<ManagedParameterDefinition> prefix = ReadNativeParameters(
command,
prefixCount,
typeMapper);
List<ManagedArgument> prefixArguments = ReadParameterArguments(prefix.Count);
GlParameterDefinition transposeSource = command.Parameters[prefixCount + 1];
GlParameterDefinition valueSource = command.Parameters[prefixCount + 2];
List<ManagedParameterDefinition> singleParameters = new List<ManagedParameterDefinition>(prefix)
{
new ManagedParameterDefinition("bool", "transpose", transposeSource),
new ManagedParameterDefinition("GraphicsMatrix", "value", valueSource),
};
List<ManagedParameterDefinition> spanParameters = new List<ManagedParameterDefinition>(prefix)
{
new ManagedParameterDefinition("bool", "transpose", transposeSource),
new ManagedParameterDefinition(
"ReadOnlySpan<GraphicsMatrix>",
"values",
valueSource),
};
return Array.AsReadOnly(
new[]
{
new ManagedMethodDefinition(
methodName,
"void",
Array.AsReadOnly(singleParameters.ToArray()),
command,
new Matrix4Invocation(
prefixArguments,
singleParameters.Count - 2,
singleParameters.Count - 1,
false)),
new ManagedMethodDefinition(
methodName,
"void",
Array.AsReadOnly(spanParameters.ToArray()),
command,
new Matrix4Invocation(
prefixArguments,
spanParameters.Count - 2,
spanParameters.Count - 1,
true)),
});
}
private static void ValidateUniformSignature(
GlCommandDefinition command,
int width,
string componentType,
int prefixCount,
bool isVector,
GlEquivalentTypeMapper typeMapper
)
{
int expectedCount = prefixCount + (isVector ? 2 : width);
if (command.Parameters.Count != expectedCount)
{
throw new InvalidOperationException(
$"Uniform command '{command.Name}' does not match its recognized shape.");
}
if (isVector)
{
GlParameterDefinition count = command.Parameters[prefixCount];
GlParameterDefinition value = command.Parameters[prefixCount + 1];
string expectedLength = $"{count.Name}*{width}";
string expectedKind = $"Vector{width}";
if (typeMapper.ReadParameterType(value) != $"{componentType}*" ||
!string.Equals(value.Length, expectedLength, StringComparison.Ordinal) &&
!(width == 1 && string.Equals(
value.Length,
count.Name,
StringComparison.Ordinal)) ||
!string.Equals(value.Kind, expectedKind, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Uniform command '{command.Name}' has inconsistent vector metadata.");
}
return;
}
for (int index = prefixCount; index < command.Parameters.Count; index++)
{
if (typeMapper.ReadParameterType(command.Parameters[index]) != componentType)
{
throw new InvalidOperationException(
$"Uniform command '{command.Name}' has inconsistent component types.");
}
}
}
private static void ValidateMatrixSignature(
GlCommandDefinition command,
int columns,
int rows,
string componentType,
int prefixCount,
GlEquivalentTypeMapper typeMapper
)
{
if (command.Parameters.Count != prefixCount + 3)
{
throw new InvalidOperationException(
$"Matrix command '{command.Name}' does not match its recognized shape.");
}
GlParameterDefinition count = command.Parameters[prefixCount];
GlParameterDefinition value = command.Parameters[prefixCount + 2];
if (typeMapper.ReadParameterType(value) != $"{componentType}*" ||
!string.Equals(
value.Kind,
$"Matrix{columns}x{rows}",
StringComparison.Ordinal) ||
!string.Equals(
value.Length,
$"{count.Name}*{columns * rows}",
StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Matrix command '{command.Name}' has inconsistent matrix metadata.");
}
}
private static List<ManagedParameterDefinition> ReadNativeParameters(
GlCommandDefinition command,
int count,
GlEquivalentTypeMapper typeMapper
)
{
List<ManagedParameterDefinition> parameters = new List<ManagedParameterDefinition>(count);
for (int index = 0; index < count; index++)
{
GlParameterDefinition source = command.Parameters[index];
parameters.Add(
new ManagedParameterDefinition(
typeMapper.ReadParameterType(source),
source.Name,
source));
}
return parameters;
}
private static List<ManagedArgument> ReadParameterArguments(
int count
)
{
List<ManagedArgument> arguments = new List<ManagedArgument>(count);
for (int index = 0; index < count; index++)
{
arguments.Add(new ParameterArgument(index));
}
return arguments;
}
private static string ReadComponentType(
string suffix
)
{
return suffix switch
{
"f" => "float",
"i" => "int",
"ui" => "uint",
"d" => "double",
_ => throw new InvalidOperationException(
$"Unsupported uniform component suffix '{suffix}'."),
};
}
}
}
@@ -0,0 +1,23 @@
// -----------------------------------------------------------------------------
// file="IGlApiFilter"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
internal interface IGlApiFilter
{
IReadOnlyList<ManagedMethodDefinition> Apply(
GlCommandDefinition command,
IReadOnlyList<ManagedMethodDefinition> methods,
GlEquivalentTypeMapper typeMapper
);
}
}
@@ -0,0 +1,80 @@
// -----------------------------------------------------------------------------
// file="ManagedApiModel"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
internal sealed record ManagedMethodDefinition(
string Name,
string ReturnType,
IReadOnlyList<ManagedParameterDefinition> Parameters,
GlCommandDefinition NativeCommand,
ManagedInvocation Invocation,
bool WrapHandleReturn = false,
bool IsEnumReturn = false,
string? DocumentationPage = null
);
internal sealed record ManagedParameterDefinition(
string Type,
string Name,
GlParameterDefinition? NativeParameter = null,
bool IsHandle = false,
bool IsHandleSpan = false,
bool IsEnum = false
);
internal abstract record ManagedInvocation;
internal sealed record DirectInvocation(
IReadOnlyList<ManagedArgument> Arguments
) : ManagedInvocation;
internal sealed record PinnedSpanInvocation(
IReadOnlyList<ManagedArgument> PrefixArguments,
int SpanParameterIndex,
string ElementType
) : ManagedInvocation;
internal sealed record PinnedVectorSpanInvocation(
IReadOnlyList<ManagedArgument> PrefixArguments,
int SpanParameterIndex,
string ComponentType,
string VectorType
) : ManagedInvocation;
internal sealed record Matrix4Invocation(
IReadOnlyList<ManagedArgument> PrefixArguments,
int TransposeParameterIndex,
int MatrixParameterIndex,
bool IsSpan
) : ManagedInvocation;
internal sealed record QuerySpanInvocation(
IReadOnlyList<ManagedArgument> PrefixArguments,
int SpanParameterIndex,
string ElementType
) : ManagedInvocation;
internal abstract record ManagedArgument;
internal sealed record ParameterArgument(
int ParameterIndex
) : ManagedArgument;
internal sealed record VectorComponentArgument(
int ParameterIndex,
string Component
) : ManagedArgument;
internal sealed record SpanLengthArgument(
int ParameterIndex
) : ManagedArgument;
}
@@ -0,0 +1,268 @@
// -----------------------------------------------------------------------------
// file="ManagedApiPlanner"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using StgSharp.GenerateGL.Analysis;
using StgSharp.GenerateGL.Naming;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
internal sealed class ManagedApiPlanner
{
private readonly IReadOnlyList<IGlApiFilter> _filters = Array.AsReadOnly<IGlApiFilter>(
new IGlApiFilter[]
{
new GlManagedApiSuppressionFilter(),
new GlOverloadApiFilter(),
new GlGetValueApiFilter(),
new GlHandleApiFilter(),
});
private readonly GlEquivalentTypeMapper _typeMapper;
private readonly GlEnumTypeMap _enumTypeMap;
private readonly GlManagedApiNamePolicy _namePolicy;
public ManagedApiPlanner(
GlEquivalentTypeMapper typeMapper,
GlFunctionFamilyAnalysis familyAnalysis,
GlEnumTypeMap enumTypeMap
)
{
ArgumentNullException.ThrowIfNull(typeMapper);
ArgumentNullException.ThrowIfNull(familyAnalysis);
ArgumentNullException.ThrowIfNull(enumTypeMap);
_typeMapper = typeMapper;
_enumTypeMap = enumTypeMap;
_namePolicy = new GlManagedApiNamePolicy(familyAnalysis);
}
public IReadOnlyList<ManagedMethodDefinition> Plan(
IReadOnlyList<GlCommandDefinition> commands
)
{
ArgumentNullException.ThrowIfNull(commands);
List<ManagedMethodDefinition> result = new List<ManagedMethodDefinition>();
foreach (GlCommandDefinition command in commands)
{
IReadOnlyList<ManagedMethodDefinition> methods = Array.AsReadOnly(
new[]
{
CreateRawMethod(command),
});
foreach (IGlApiFilter filter in _filters)
{
methods = filter.Apply(command, methods, _typeMapper);
}
methods = ApplyEnumTypes(command, methods);
result.AddRange(
methods.Select(
method => method with
{
Name = _namePolicy.ReadMethodName(command, method.Name),
DocumentationPage = _namePolicy.ReadDocumentationPage(command),
Parameters = Array.AsReadOnly(
method.Parameters.Select(
parameter => parameter with
{
Name = CSharpIdentifierPolicy.NormalizeParameterName(
parameter.Name),
}).ToArray()),
}));
}
ResolveNameCollisions(result);
return Array.AsReadOnly(result.ToArray());
}
private ReadOnlyCollection<ManagedMethodDefinition> ApplyEnumTypes(
GlCommandDefinition command,
IReadOnlyList<ManagedMethodDefinition> methods
)
{
bool hasEnumReturn = _enumTypeMap.TryReadReturnType(
command.Return,
out string enumReturnType);
return Array.AsReadOnly(
methods.Select(
method => ConvertEnumReturn(method, hasEnumReturn, enumReturnType)).ToArray());
}
private ManagedMethodDefinition ConvertEnumReturn(
ManagedMethodDefinition method,
bool hasEnumReturn,
string enumReturnType
)
{
string returnType = string.Empty;
bool isEnumReturn = hasEnumReturn && TryReplaceNativeEnumType(
method.ReturnType,
enumReturnType,
out returnType);
return method with
{
ReturnType = isEnumReturn ? returnType : method.ReturnType,
IsEnumReturn = isEnumReturn,
Parameters = Array.AsReadOnly(
method.Parameters.Select(ConvertEnumParameter).ToArray()),
};
}
private ManagedParameterDefinition ConvertEnumParameter(
ManagedParameterDefinition parameter
)
{
if (parameter.NativeParameter is null ||
!_enumTypeMap.TryReadParameterType(
parameter.NativeParameter,
out string enumType) ||
!TryReplaceNativeEnumType(parameter.Type, enumType, out string convertedType))
{
return parameter;
}
return parameter with
{
Type = convertedType,
IsEnum = true,
};
}
private static bool TryReplaceNativeEnumType(
string nativeType,
string enumType,
out string convertedType
)
{
const string nativeBaseType = "uint";
if (!nativeType.StartsWith(nativeBaseType, StringComparison.Ordinal) ||
nativeType.AsSpan(nativeBaseType.Length).ContainsAnyExcept('*'))
{
convertedType = string.Empty;
return false;
}
convertedType = enumType + nativeType[nativeBaseType.Length..];
return true;
}
public static void Validate(
IEnumerable<ManagedMethodDefinition> methods
)
{
ArgumentNullException.ThrowIfNull(methods);
Dictionary<string, ManagedMethodDefinition> signatures = new Dictionary<string, ManagedMethodDefinition>(
StringComparer.Ordinal);
foreach (ManagedMethodDefinition method in methods)
{
string signature = $"{method.Name}({string.Join(",", method.Parameters.Select(value => value.Type))})";
if (signatures.TryGetValue(signature, out ManagedMethodDefinition? previous))
{
throw new InvalidOperationException(
$"Managed overload '{signature}' is produced by both " +
$"'{previous.NativeCommand.Name}' and '{method.NativeCommand.Name}'.");
}
signatures.Add(signature, method);
}
}
private static void ResolveNameCollisions(
List<ManagedMethodDefinition> methods
)
{
ILookup<string, int> indicesBySignature = methods
.Select(
(method, index) => new
{
Index = index,
Signature = ReadManagedSignature(method),
})
.ToLookup(value => value.Signature, value => value.Index, StringComparer.Ordinal);
foreach (IGrouping<string, int> group in indicesBySignature)
{
int[] indices = group.ToArray();
if (indices.Length < 2)
{
continue;
}
string[] nativeNames = indices
.Select(value => methods[value].NativeCommand.Name)
.Distinct(StringComparer.Ordinal)
.ToArray();
if (nativeNames.Length != indices.Length)
{
throw new InvalidOperationException(
$"Managed overload '{group.Key}' is duplicated by one native command.");
}
foreach (int index in indices)
{
ManagedMethodDefinition method = methods[index];
methods[index] = method with
{
Name = GlManagedApiNamePolicy.ReadNativeMethodName(
method.NativeCommand),
};
}
}
}
private static string ReadManagedSignature(
ManagedMethodDefinition method
)
{
return $"{method.Name}({string.Join(",", method.Parameters.Select(value => value.Type))})";
}
private ManagedMethodDefinition CreateRawMethod(
GlCommandDefinition command
)
{
List<ManagedParameterDefinition> parameters = new List<ManagedParameterDefinition>(
command.Parameters.Count);
List<ManagedArgument> arguments = new List<ManagedArgument>(
command.Parameters.Count);
for (int index = 0; index < command.Parameters.Count; index++)
{
GlParameterDefinition source = command.Parameters[index];
parameters.Add(
new ManagedParameterDefinition(
_typeMapper.ReadParameterType(source),
source.Name,
source));
arguments.Add(new ParameterArgument(index));
}
return new ManagedMethodDefinition(
ReadMethodName(command.Name),
_typeMapper.ReadReturnType(command),
Array.AsReadOnly(parameters.ToArray()),
command,
new DirectInvocation(Array.AsReadOnly(arguments.ToArray())));
}
private static string ReadMethodName(
string commandName
)
{
return commandName.StartsWith("gl", StringComparison.Ordinal) &&
commandName.Length > 2
? commandName[2..]
: commandName;
}
}
}
@@ -0,0 +1,24 @@
// -----------------------------------------------------------------------------
// file="ManagedEnumModel"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
internal sealed record ManagedEnumDefinition(
string Name,
bool IsFlags,
IReadOnlyList<string> NativeGroups,
IReadOnlyList<ManagedEnumMemberDefinition> Members
);
internal sealed record ManagedEnumMemberDefinition(
string Name,
GlEnumDefinition NativeValue
);
}
@@ -0,0 +1,916 @@
// -----------------------------------------------------------------------------
// file="OpenGlGenerator"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using StgSharp.GenerateGL.Analysis;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Generation
{
/// <summary>
/// Produces the first in-memory desktop OpenGL binding files.
/// </summary>
internal static class OpenGlGenerator
{
private static readonly HashSet<string> _csharpKeywords = new HashSet<string>(
new[]
{
"abstract", "as", "base", "bool", "break", "byte", "case", "catch",
"char", "checked", "class", "const", "continue", "decimal", "default",
"delegate", "do", "double", "else", "enum", "event", "explicit", "extern",
"false", "finally", "fixed", "float", "for", "foreach", "goto", "if",
"implicit", "in", "int", "interface", "internal", "is", "lock", "long",
"namespace", "new", "null", "object", "operator", "out", "override",
"params", "private", "protected", "public", "readonly", "ref", "return",
"sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string",
"struct", "switch", "this", "throw", "true", "try", "typeof", "uint",
"ulong", "unchecked", "unsafe", "ushort", "using", "virtual", "void",
"volatile", "while",
},
StringComparer.Ordinal);
public static GeneratedFileSet Generate(
GlRegistryProjection projection,
GlGenerationTarget target = GlGenerationTarget.All
)
{
ArgumentNullException.ThrowIfNull(projection);
if (target == GlGenerationTarget.None || (target & ~GlGenerationTarget.All) != 0)
{
throw new ArgumentOutOfRangeException(nameof(target));
}
IReadOnlyList<ManagedEnumDefinition> enums = GlEnumProjector.Project(projection);
GlEnumTypeMap enumTypeMap = new GlEnumTypeMap(enums);
List<GeneratedFile> files = new List<GeneratedFile>();
if ((target & GlGenerationTarget.Enum) != 0)
{
files.Add(new GeneratedFile("glconst.cs", GenerateConstants(projection.Enums)));
files.Add(new GeneratedFile("GlEnums.g.cs", GenerateEnums(enums)));
}
if ((target & GlGenerationTarget.Api) != 0)
{
IReadOnlyList<GlCommandGroup> commandGroups = ReadCoreCommandGroups(projection);
IReadOnlyList<GlCommandDefinition> commands = Array.AsReadOnly(
commandGroups.SelectMany(value => value.Commands).ToArray());
GlEquivalentTypeMapper typeMapper = new GlEquivalentTypeMapper(projection.Source);
GlFunctionFamilyAnalysis familyAnalysis = GlFunctionFamilyAnalyzer.Analyze(projection);
ManagedApiPlanner apiPlanner = new ManagedApiPlanner(
typeMapper,
familyAnalysis,
enumTypeMap);
IReadOnlyList<ManagedMethodDefinition> managedMethods = apiPlanner.Plan(commands);
Dictionary<string, IReadOnlyList<ManagedMethodDefinition>> methodsByCommand =
managedMethods.GroupBy(value => value.NativeCommand.Name, StringComparer.Ordinal)
.ToDictionary(
value => value.Key,
value => (IReadOnlyList<ManagedMethodDefinition>)Array.AsReadOnly(
value.ToArray()),
StringComparer.Ordinal);
List<ManagedCommandGroup> managedGroups = commandGroups.Select(
group => new ManagedCommandGroup(
group,
Array.AsReadOnly(
group.Commands.SelectMany(
command => methodsByCommand.TryGetValue(
command.Name,
out IReadOnlyList<ManagedMethodDefinition>? methods)
? methods
: Array.Empty<ManagedMethodDefinition>())
.ToArray()))).ToList();
ManagedApiPlanner.Validate(
managedGroups.SelectMany(value => value.Methods));
files.Add(new GeneratedFile(
"OpenglContext.g.cs",
GenerateContext(commands, typeMapper)));
files.Add(new GeneratedFile(
"glContextShadow.g.cs",
GenerateContextShadow(commands)));
for (int index = 0; index < managedGroups.Count; index++)
{
ManagedCommandGroup managedGroup = managedGroups[index];
GlCommandGroup group = managedGroup.Source;
files.Add(
new GeneratedFile(
group.FileName,
GenerateFunctions(
managedGroup.Methods,
group.FileName,
group.Description,
index == 0)));
}
}
return new GeneratedFileSet(files);
}
private static ReadOnlyCollection<GlCommandGroup> ReadCoreCommandGroups(
GlRegistryProjection projection
)
{
List<GlCommandDefinition> foundation = new List<GlCommandDefinition>();
Dictionary<string, List<GlCommandDefinition>> versionGroups = new Dictionary<string, List<GlCommandDefinition>>(
StringComparer.Ordinal);
List<string> versionOrder = new List<string>();
foreach (GlCommandDefinition command in projection.Commands)
{
if (!projection.CommandIntroducedVersions.TryGetValue(
command.Name,
out string? version))
{
throw new InvalidDataException(
$"Command '{command.Name}' has no introduction version.");
}
if (IsFoundationVersion(version))
{
foundation.Add(command);
continue;
}
if (!versionGroups.TryGetValue(
version,
out List<GlCommandDefinition>? commands))
{
commands = new List<GlCommandDefinition>();
versionGroups.Add(version, commands);
versionOrder.Add(version);
}
commands.Add(command);
}
List<GlCommandGroup> groups = new List<GlCommandGroup>
{
new GlCommandGroup(
"OpenGLFunction.Base.g.cs",
"Desktop OpenGL forwarding methods introduced through 3.2 and retained by the target core profile",
Array.AsReadOnly(foundation.ToArray())),
};
foreach (string version in versionOrder)
{
groups.Add(
new GlCommandGroup(
$"OpenGLFunction.v{version.Replace('.', '_')}.g.cs",
$"Desktop OpenGL {version} core-profile forwarding methods",
Array.AsReadOnly(versionGroups[version].ToArray())));
}
return Array.AsReadOnly(groups.ToArray());
}
private static bool IsFoundationVersion(
string version
)
{
string[] components = version.Split('.');
if (components.Length != 2 ||
!int.TryParse(
components[0],
NumberStyles.None,
CultureInfo.InvariantCulture,
out int major) ||
!int.TryParse(
components[1],
NumberStyles.None,
CultureInfo.InvariantCulture,
out int minor))
{
throw new InvalidDataException(
$"OpenGL feature version '{version}' is not a major.minor number.");
}
return major < 3 || major == 3 && minor <= 2;
}
private static string GenerateConstants(
IReadOnlyList<GlEnumDefinition> enums
)
{
StringBuilder builder = new StringBuilder(512 * 1024);
AppendGeneratedHeader(builder, "glconst.cs", "Desktop OpenGL constants");
builder.Append("namespace StgSharp.Graphics.OpenGL\n");
builder.Append("{\n");
builder.Append(" public static class glConst\n");
builder.Append(" {\n\n");
Dictionary<string, GlEnumDefinition> emitted = new Dictionary<string, GlEnumDefinition>(
StringComparer.Ordinal);
foreach (GlEnumDefinition value in enums)
{
if (value.Value is null)
{
continue;
}
if (emitted.TryGetValue(value.Name, out GlEnumDefinition? previous))
{
if (!string.Equals(previous.Value, value.Value, StringComparison.Ordinal))
{
throw new InvalidDataException(
$"Desktop enum '{value.Name}' has conflicting values " +
$"'{previous.Value}' and '{value.Value}'.");
}
continue;
}
emitted.Add(value.Name, value);
ConstantExpression expression = ReadConstantExpression(value);
builder.Append(" public const ");
builder.Append(expression.Type);
builder.Append(' ');
builder.Append(ReadConstantName(value.Name));
builder.Append(" = ");
builder.Append(expression.Literal);
builder.Append(";\n");
}
builder.Append("\n }\n");
builder.Append("}\n");
return builder.ToString();
}
private static string GenerateEnums(
IReadOnlyList<ManagedEnumDefinition> enums
)
{
StringBuilder builder = new StringBuilder(512 * 1024);
AppendGeneratedHeader(
builder,
"GlEnums.g.cs",
"Strongly typed desktop OpenGL enum groups");
builder.Append("namespace StgSharp.Graphics.OpenGL\n");
builder.Append("{\n");
foreach (ManagedEnumDefinition value in enums)
{
builder.Append(" /// <summary>Native OpenGL group");
builder.Append(value.NativeGroups.Count == 1 ? " " : "s ");
builder.Append(string.Join(", ", value.NativeGroups));
builder.Append(".</summary>\n");
if (value.IsFlags)
{
builder.Append(" [System.Flags]\n");
}
builder.Append(" public enum ");
builder.Append(value.Name);
builder.Append(" : uint\n");
builder.Append(" {\n");
foreach (ManagedEnumMemberDefinition member in value.Members)
{
builder.Append(" ");
builder.Append(member.Name);
builder.Append(" = ");
if (member.NativeValue.Name == "GL_NONE" &&
member.NativeValue.Comment ==
"Synthetic zero value for a generated flags enum.")
{
builder.Append("0u");
}
else
{
builder.Append("unchecked((uint)glConst.");
builder.Append(ReadConstantName(member.NativeValue.Name));
builder.Append(')');
}
builder.Append(",\n");
}
builder.Append(" }\n\n");
}
builder.Append("}\n");
return builder.ToString();
}
private static ConstantExpression ReadConstantExpression(
GlEnumDefinition value
)
{
string source = value.Value!;
bool isHex = source.StartsWith("0x", StringComparison.OrdinalIgnoreCase);
BigInteger number = isHex
? BigInteger.Parse(
$"0{source[2..]}",
NumberStyles.AllowHexSpecifier,
CultureInfo.InvariantCulture)
: BigInteger.Parse(source, NumberStyles.Integer, CultureInfo.InvariantCulture);
if (string.Equals(value.TypeSuffix, "ull", StringComparison.OrdinalIgnoreCase))
{
EnsureRange(number, BigInteger.Zero, ulong.MaxValue, value);
return new ConstantExpression("ulong", ReadLiteral(source, isHex, "UL"));
}
if (string.Equals(value.TypeSuffix, "u", StringComparison.OrdinalIgnoreCase))
{
EnsureRange(number, BigInteger.Zero, uint.MaxValue, value);
return new ConstantExpression("uint", ReadLiteral(source, isHex, "u"));
}
if (number >= int.MinValue && number <= int.MaxValue)
{
return new ConstantExpression("int", source);
}
if (number >= BigInteger.Zero && number <= uint.MaxValue)
{
return new ConstantExpression("uint", ReadLiteral(source, isHex, "u"));
}
if (number >= long.MinValue && number <= long.MaxValue)
{
return new ConstantExpression("long", ReadLiteral(source, isHex, "L"));
}
if (number >= BigInteger.Zero && number <= ulong.MaxValue)
{
return new ConstantExpression("ulong", ReadLiteral(source, isHex, "UL"));
}
throw new InvalidDataException(
$"Enum '{value.Name}' value '{source}' does not fit a C# integral type.");
}
private static void EnsureRange(
BigInteger value,
BigInteger minimum,
BigInteger maximum,
GlEnumDefinition definition
)
{
if (value < minimum || value > maximum)
{
throw new InvalidDataException(
$"Enum '{definition.Name}' value '{definition.Value}' is outside its " +
"declared numeric suffix range.");
}
}
private static string ReadLiteral(
string source,
bool isHex,
string suffix
)
{
return isHex ? $"{source}{suffix}" : $"{source}{suffix}";
}
private static string ReadConstantName(
string name
)
{
string value = name.StartsWith("GL_", StringComparison.Ordinal)
? name[3..]
: name;
StringBuilder result = new StringBuilder(value.Length + 1);
if (value.Length == 0 || char.IsDigit(value[0]))
{
result.Append('_');
}
foreach (char character in value)
{
result.Append(char.IsLetterOrDigit(character) || character == '_'
? character
: '_');
}
return result.ToString();
}
private static string GenerateContext(
IReadOnlyList<GlCommandDefinition> commands,
GlEquivalentTypeMapper typeMapper
)
{
StringBuilder builder = new StringBuilder(128 * 1024);
AppendGeneratedHeader(
builder,
"OpenglContext.g.cs",
"Desktop OpenGL core function pointer table");
builder.Append("namespace StgSharp.Graphics.OpenGL\n");
builder.Append("{\n");
builder.Append(" [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]\n");
builder.Append(" public unsafe partial struct OpenglContext\n");
builder.Append(" {\n");
foreach (GlCommandDefinition command in OrderCommands(commands))
{
builder.Append(" internal delegate* unmanaged<");
foreach (GlParameterDefinition parameter in command.Parameters)
{
builder.Append(typeMapper.ReadParameterType(parameter));
builder.Append(", ");
}
builder.Append(typeMapper.ReadReturnType(command));
builder.Append("> ");
builder.Append(command.Name);
builder.Append(";\n");
}
builder.Append(" }\n");
builder.Append("}\n");
return builder.ToString();
}
private static string GenerateContextShadow(
IReadOnlyList<GlCommandDefinition> commands
)
{
StringBuilder builder = new StringBuilder(64 * 1024);
AppendGeneratedHeader(
builder,
"glContextShadow.g.cs",
"Native-address shadow of the OpenGL function pointer table");
builder.Append("namespace StgSharp.Graphics.OpenGL\n");
builder.Append("{\n");
builder.Append(" [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]\n");
builder.Append(" internal struct glContextShadow\n");
builder.Append(" {\n");
foreach (GlCommandDefinition command in OrderCommands(commands))
{
builder.Append(" internal nint ");
builder.Append(command.Name);
builder.Append(";\n");
}
builder.Append(" }\n");
builder.Append("}\n");
return builder.ToString();
}
private static IEnumerable<GlCommandDefinition> OrderCommands(
IReadOnlyList<GlCommandDefinition> commands
)
{
return commands.OrderBy(
value => value.Name,
StringComparer.Ordinal);
}
private static string GenerateFunctions(
IReadOnlyList<ManagedMethodDefinition> methods,
string outputFileName,
string description,
bool includeContextState
)
{
StringBuilder builder = new StringBuilder(512 * 1024);
AppendGeneratedHeader(builder, outputFileName, description);
builder.Append("using StgSharp.Mathematics.Numeric.Graphics;\n\n");
builder.Append("using System;\n");
builder.Append("using System.Runtime.CompilerServices;\n\n");
builder.Append("namespace StgSharp.Graphics.OpenGL\n");
builder.Append("{\n");
builder.Append(" public unsafe partial class OpenGLFunction\n");
builder.Append(" {\n");
if (includeContextState)
{
builder.Append("\n private readonly OpenglContext* __context;\n");
builder.Append("\n internal OpenGLFunction(OpenglContext* context)\n");
builder.Append(" {\n");
builder.Append(" __context = context;\n");
builder.Append(" }\n");
}
foreach (ManagedMethodDefinition method in methods)
{
AppendMethodDocumentation(builder, method);
builder.Append("\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n");
builder.Append(" public ");
builder.Append(method.ReturnType);
builder.Append(' ');
builder.Append(method.Name);
builder.Append('(');
for (int index = 0; index < method.Parameters.Count; index++)
{
if (index != 0)
{
builder.Append(", ");
}
ManagedParameterDefinition parameter = method.Parameters[index];
builder.Append(parameter.Type);
builder.Append(' ');
builder.Append(EscapeIdentifier(parameter.Name));
}
builder.Append(")\n");
builder.Append(" {\n");
AppendInvocation(builder, method);
builder.Append(" }\n");
}
builder.Append("\n }\n");
builder.Append("}\n");
return builder.ToString();
}
private static void AppendMethodDocumentation(
StringBuilder builder,
ManagedMethodDefinition method
)
{
string declaration = ReadNativeDeclaration(method.NativeCommand);
string page = method.DocumentationPage ?? method.NativeCommand.Name;
builder.Append("\n /// <summary>\n");
builder.Append(" /// <para>Forwards to the native OpenGL entry point.</para>\n");
builder.Append(" /// <para><c>");
builder.Append(EscapeXml(declaration));
builder.Append("</c></para>\n");
builder.Append(" /// <para>See <see href=\"https://docs.gl/gl4/");
builder.Append(page);
builder.Append("\">docs.gl</see>.</para>\n");
builder.Append(" /// </summary>");
}
private static string ReadNativeDeclaration(
GlCommandDefinition command
)
{
return $"{command.Return.DeclarationText}(" +
string.Join(", ", command.Parameters.Select(value => value.DeclarationText)) +
");";
}
private static string EscapeXml(
string value
)
{
return value.Replace("&", "&amp;", StringComparison.Ordinal)
.Replace("<", "&lt;", StringComparison.Ordinal)
.Replace(">", "&gt;", StringComparison.Ordinal)
.Replace("\"", "&quot;", StringComparison.Ordinal)
.Replace("'", "&apos;", StringComparison.Ordinal);
}
private static void AppendInvocation(
StringBuilder builder,
ManagedMethodDefinition method
)
{
switch (method.Invocation)
{
case DirectInvocation direct:
AppendDirectInvocation(builder, method, direct);
break;
case PinnedSpanInvocation pinned:
AppendPinnedSpanInvocation(builder, method, pinned);
break;
case PinnedVectorSpanInvocation pinnedVector:
AppendPinnedVectorSpanInvocation(builder, method, pinnedVector);
break;
case Matrix4Invocation matrix:
AppendMatrix4Invocation(builder, method, matrix);
break;
case QuerySpanInvocation query:
AppendQuerySpanInvocation(builder, method, query);
break;
default:
throw new InvalidOperationException(
$"Unsupported managed invocation '{method.Invocation.GetType().Name}'.");
}
}
private static void AppendQuerySpanInvocation(
StringBuilder builder,
ManagedMethodDefinition method,
QuerySpanInvocation invocation
)
{
ManagedParameterDefinition span = method.Parameters[invocation.SpanParameterIndex];
string spanName = EscapeIdentifier(span.Name);
builder.Append(" fixed (");
builder.Append(invocation.ElementType);
builder.Append("* __value = ");
builder.Append(spanName);
builder.Append(")\n");
builder.Append(" {\n");
builder.Append(" __context->");
builder.Append(method.NativeCommand.Name);
builder.Append('(');
AppendArguments(builder, method, invocation.PrefixArguments);
if (invocation.PrefixArguments.Count != 0)
{
builder.Append(", ");
}
builder.Append("__value);\n");
builder.Append(" }\n");
}
private static void AppendDirectInvocation(
StringBuilder builder,
ManagedMethodDefinition method,
DirectInvocation invocation
)
{
List<int> fixedParameters = new List<int>();
for (int index = 0; index < method.Parameters.Count; index++)
{
if (method.Parameters[index].IsHandleSpan)
{
fixedParameters.Add(index);
}
}
int indentation = 12;
foreach (int parameterIndex in fixedParameters)
{
ManagedParameterDefinition parameter = method.Parameters[parameterIndex];
builder.Append(' ', indentation);
builder.Append("fixed (GlHandle* __");
builder.Append(parameter.Name);
builder.Append(" = ");
builder.Append(EscapeIdentifier(parameter.Name));
builder.Append(")\n");
builder.Append(' ', indentation);
builder.Append("{\n");
indentation += 4;
}
builder.Append(' ', indentation);
if (method.WrapHandleReturn)
{
builder.Append("return GlHandle.Create(");
}
else if (method.IsEnumReturn)
{
builder.Append("return (");
builder.Append(method.ReturnType);
builder.Append(')');
}
else if (method.ReturnType != "void")
{
builder.Append("return ");
}
AppendNativeCall(builder, method, invocation.Arguments);
builder.Append(method.WrapHandleReturn ? ");\n" : ";\n");
for (int index = fixedParameters.Count - 1; index >= 0; index--)
{
indentation -= 4;
builder.Append(' ', indentation);
builder.Append("}\n");
}
}
private static void AppendPinnedSpanInvocation(
StringBuilder builder,
ManagedMethodDefinition method,
PinnedSpanInvocation invocation
)
{
ManagedParameterDefinition span = method.Parameters[invocation.SpanParameterIndex];
builder.Append(" fixed (");
builder.Append(invocation.ElementType);
builder.Append("* __value = ");
builder.Append(EscapeIdentifier(span.Name));
builder.Append(")\n");
builder.Append(" {\n");
builder.Append(" __context->");
builder.Append(method.NativeCommand.Name);
builder.Append('(');
AppendArguments(builder, method, invocation.PrefixArguments);
if (invocation.PrefixArguments.Count != 0)
{
builder.Append(", ");
}
builder.Append(EscapeIdentifier(span.Name));
builder.Append(".Length, __value);\n");
builder.Append(" }\n");
}
private static void AppendPinnedVectorSpanInvocation(
StringBuilder builder,
ManagedMethodDefinition method,
PinnedVectorSpanInvocation invocation
)
{
ManagedParameterDefinition span = method.Parameters[invocation.SpanParameterIndex];
string spanName = EscapeIdentifier(span.Name);
builder.Append(" fixed (");
builder.Append(invocation.VectorType);
builder.Append("* __vector = ");
builder.Append(spanName);
builder.Append(")\n");
builder.Append(" {\n");
builder.Append(" __context->");
builder.Append(method.NativeCommand.Name);
builder.Append('(');
AppendArguments(builder, method, invocation.PrefixArguments);
if (invocation.PrefixArguments.Count != 0)
{
builder.Append(", ");
}
builder.Append(spanName);
builder.Append(".Length, (");
builder.Append(invocation.ComponentType);
builder.Append("*)__vector);\n");
builder.Append(" }\n");
}
private static void AppendMatrix4Invocation(
StringBuilder builder,
ManagedMethodDefinition method,
Matrix4Invocation invocation
)
{
ManagedParameterDefinition transpose = method.Parameters[invocation.TransposeParameterIndex];
ManagedParameterDefinition matrix = method.Parameters[invocation.MatrixParameterIndex];
string transposeName = EscapeIdentifier(transpose.Name);
string matrixName = EscapeIdentifier(matrix.Name);
if (invocation.IsSpan)
{
builder.Append(" fixed (GraphicsMatrix* __value = ");
builder.Append(matrixName);
builder.Append(")\n");
builder.Append(" {\n");
builder.Append(" __context->");
}
else
{
builder.Append(" __context->");
}
builder.Append(method.NativeCommand.Name);
builder.Append('(');
AppendArguments(builder, method, invocation.PrefixArguments);
if (invocation.PrefixArguments.Count != 0)
{
builder.Append(", ");
}
builder.Append(invocation.IsSpan ? $"{matrixName}.Length" : "1");
builder.Append(", ");
builder.Append(transposeName);
builder.Append(" ? (byte)1 : (byte)0, (float*)");
builder.Append(invocation.IsSpan ? "__value" : $"&{matrixName}");
builder.Append(");\n");
if (invocation.IsSpan)
{
builder.Append(" }\n");
}
}
private static void AppendNativeCall(
StringBuilder builder,
ManagedMethodDefinition method,
IReadOnlyList<ManagedArgument> arguments
)
{
builder.Append("__context->");
builder.Append(method.NativeCommand.Name);
builder.Append('(');
AppendArguments(builder, method, arguments);
builder.Append(')');
}
private static void AppendArguments(
StringBuilder builder,
ManagedMethodDefinition method,
IReadOnlyList<ManagedArgument> arguments
)
{
for (int index = 0; index < arguments.Count; index++)
{
if (index != 0)
{
builder.Append(", ");
}
switch (arguments[index])
{
case ParameterArgument parameterArgument:
ManagedParameterDefinition parameter = method.Parameters[
parameterArgument.ParameterIndex];
string parameterName = EscapeIdentifier(parameter.Name);
if (parameter.IsEnum)
{
builder.Append("(uint");
builder.Append('*', parameter.Type.Count(value => value == '*'));
builder.Append(')');
builder.Append(parameterName);
}
else if (!parameter.IsHandle)
{
builder.Append(parameterName);
}
else if (parameter.Type.EndsWith('*'))
{
builder.Append("(uint*)");
builder.Append(parameterName);
}
else if (parameter.IsHandleSpan)
{
builder.Append("(uint*)__");
builder.Append(parameter.Name);
}
else
{
builder.Append(parameterName);
builder.Append(".Value");
}
break;
case SpanLengthArgument spanLengthArgument:
builder.Append(
EscapeIdentifier(
method.Parameters[spanLengthArgument.ParameterIndex].Name));
builder.Append(".Length");
break;
case VectorComponentArgument componentArgument:
builder.Append(
EscapeIdentifier(
method.Parameters[componentArgument.ParameterIndex].Name));
builder.Append('.');
builder.Append(componentArgument.Component);
break;
default:
throw new InvalidOperationException(
$"Unsupported managed argument '{arguments[index].GetType().Name}'.");
}
}
}
private static string EscapeIdentifier(
string value
)
{
return _csharpKeywords.Contains(value) ? $"@{value}" : value;
}
private static void AppendGeneratedHeader(
StringBuilder builder,
string outputFileName,
string description
)
{
builder.Append("// -----------------------------------------------------------------------------\n");
builder.Append("// file=\"");
builder.Append(ReadGeneratedFileName(outputFileName));
builder.Append("\"\n");
builder.Append("// Project: StgSharp\n");
builder.Append("// Copyright (c) Nitload.\n");
builder.Append("// SPDX-License-Identifier: MIT\n");
builder.Append("// -----------------------------------------------------------------------------\n\n");
builder.Append("// <auto-generated>\n");
builder.Append("// Generated by StgSharp.GenerateGL.\n");
builder.Append("// ");
builder.Append(description);
builder.Append(".\n");
builder.Append("// </auto-generated>\n\n");
}
private static string ReadGeneratedFileName(
string outputFileName
)
{
const string generatedExtension = ".g.cs";
const string sourceExtension = ".cs";
if (outputFileName.EndsWith(generatedExtension, StringComparison.Ordinal))
{
return outputFileName[..^generatedExtension.Length];
}
return outputFileName.EndsWith(sourceExtension, StringComparison.Ordinal)
? outputFileName[..^sourceExtension.Length]
: outputFileName;
}
private sealed record ConstantExpression(
string Type,
string Literal
);
private sealed record GlCommandGroup(
string FileName,
string Description,
IReadOnlyList<GlCommandDefinition> Commands
);
private sealed record ManagedCommandGroup(
GlCommandGroup Source,
IReadOnlyList<ManagedMethodDefinition> Methods
);
}
}
@@ -0,0 +1,114 @@
// -----------------------------------------------------------------------------
// file="CSharpIdentifierPolicy"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Text.RegularExpressions;
namespace StgSharp.GenerateGL.Naming
{
internal static class CSharpIdentifierPolicy
{
private static readonly Regex _attributeWord = new Regex(
@"Attrib(?!ute)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex _attributeParameterWord = new Regex(
@"attrib(?!ute)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex _fragmentWord = new Regex(
@"Frag(?!ment)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public static string NormalizeMemberName(
string value
)
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
string result = value.Replace(
"Framebuffer",
"FrameBuffer",
StringComparison.Ordinal)
.Replace(
"Renderbuffer",
"RenderBuffer",
StringComparison.Ordinal)
.Replace(
"Internalformat",
"InternalFormat",
StringComparison.Ordinal);
result = _attributeWord.Replace(result, "Attribute");
return _fragmentWord.Replace(result, "Fragment");
}
public static string NormalizeParameterName(
string value
)
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
string result = value.Replace(
"Framebuffer",
"FrameBuffer",
StringComparison.Ordinal)
.Replace(
"framebuffer",
"frameBuffer",
StringComparison.Ordinal)
.Replace(
"Renderbuffer",
"RenderBuffer",
StringComparison.Ordinal)
.Replace(
"renderbuffer",
"renderBuffer",
StringComparison.Ordinal)
.Replace(
"Internalformat",
"InternalFormat",
StringComparison.Ordinal)
.Replace(
"internalformat",
"internalFormat",
StringComparison.Ordinal)
.Replace(
"attribindex",
"attributeIndex",
StringComparison.Ordinal)
.Replace(
"bindingindex",
"bindingIndex",
StringComparison.Ordinal)
.Replace(
"relativeoffset",
"relativeOffset",
StringComparison.Ordinal)
.Replace(
"pname",
"pName",
StringComparison.Ordinal)
.Replace(
"xoffset",
"xOffset",
StringComparison.Ordinal)
.Replace(
"yoffset",
"yOffset",
StringComparison.Ordinal)
.Replace(
"zoffset",
"zOffset",
StringComparison.Ordinal)
.Replace(
"vaobj",
"vertexArray",
StringComparison.Ordinal);
return _attributeParameterWord.Replace(result, "attribute");
}
}
}
@@ -0,0 +1,345 @@
// -----------------------------------------------------------------------------
// file="GlManagedApiNamePolicy"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using StgSharp.GenerateGL.Analysis;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Naming
{
/// <summary>
/// Translates native overload families into stable, semantic C# method names.
/// </summary>
internal sealed class GlManagedApiNamePolicy
{
private static readonly Regex _getVertexAttributeModePattern = new Regex(
@"^glGetVertexAttrib(?<mode>I|L)(?:i|ui|d)v$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex _vertexAttributeModePattern = new Regex(
@"^glVertexAttrib(?<mode>I|L|P)[1-4]",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly Dictionary<string, FamilyEntry> _familiesByCommand;
public GlManagedApiNamePolicy(
GlFunctionFamilyAnalysis analysis
)
{
ArgumentNullException.ThrowIfNull(analysis);
_familiesByCommand = analysis.Families
.SelectMany(
family => family.Members.Select(
member => new KeyValuePair<string, FamilyEntry>(
member.Command.Name,
new FamilyEntry(family, member.NameShape))))
.ToDictionary(
value => value.Key,
value => value.Value,
StringComparer.Ordinal);
}
public string ReadMethodName(
GlCommandDefinition command,
string currentName
)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentException.ThrowIfNullOrWhiteSpace(currentName);
string rawName = CSharpIdentifierPolicy.NormalizeMemberName(
RemoveGlPrefix(command.Name));
string normalizedCurrentName = CSharpIdentifierPolicy.NormalizeMemberName(
currentName);
if (!string.Equals(normalizedCurrentName, rawName, StringComparison.Ordinal))
{
return normalizedCurrentName;
}
string? specialName = ReadSpecialMethodName(command.Name);
if (specialName is not null)
{
return specialName;
}
if (!_familiesByCommand.TryGetValue(
command.Name,
out FamilyEntry? entry))
{
return rawName;
}
string familyName = ReadSemanticFamilyName(entry.Family.Name);
if (entry.Family.ShapeNamePolicy == GlFunctionShapeNamePolicy.MustRetain &&
entry.Shape?.Shape is not null)
{
familyName += entry.Shape.Shape;
}
return CSharpIdentifierPolicy.NormalizeMemberName(familyName);
}
public static string ReadNativeMethodName(
GlCommandDefinition command
)
{
ArgumentNullException.ThrowIfNull(command);
return CSharpIdentifierPolicy.NormalizeMemberName(RemoveGlPrefix(command.Name));
}
public string ReadDocumentationPage(
GlCommandDefinition command
)
{
ArgumentNullException.ThrowIfNull(command);
string? specialPage = ReadSpecialDocumentationPage(command.Name);
if (specialPage is not null)
{
return specialPage;
}
if (!_familiesByCommand.TryGetValue(
command.Name,
out FamilyEntry? entry))
{
return command.Name;
}
string familyName = entry.Family.Name
.Replace("FrameBuffer", "Framebuffer", StringComparison.Ordinal)
.Replace("RenderBuffer", "Renderbuffer", StringComparison.Ordinal)
.Replace("InternalFormat", "Internalformat", StringComparison.Ordinal)
.Replace("Attribute", "Attrib", StringComparison.Ordinal)
.Replace("Fragment", "Frag", StringComparison.Ordinal);
return $"gl{ReadDocumentationFamilyName(familyName)}";
}
private static string? ReadSpecialMethodName(
string commandName
)
{
if (commandName == "glClearBufferfi")
{
return "ClearBuffer";
}
if (commandName == "glClearNamedFramebufferfi")
{
return "ClearNamedFrameBuffer";
}
if (commandName == "glGetVertexAttribPointerv")
{
return "GetVertexAttributePointer";
}
if (commandName.StartsWith("glVertexAttrib4N", StringComparison.Ordinal))
{
return "VertexAttributeNormalized";
}
Match getMatch = _getVertexAttributeModePattern.Match(commandName);
if (getMatch.Success)
{
return getMatch.Groups["mode"].Value == "I"
? "GetVertexAttributeInteger"
: "GetVertexAttributeDouble";
}
if (commandName is "glVertexAttribIFormat" or "glVertexArrayAttribIFormat")
{
return CSharpIdentifierPolicy.NormalizeMemberName(
RemoveGlPrefix(commandName).Replace(
"AttribIFormat",
"AttributeIntegerFormat",
StringComparison.Ordinal));
}
if (commandName is "glVertexAttribLFormat" or "glVertexArrayAttribLFormat")
{
return CSharpIdentifierPolicy.NormalizeMemberName(
RemoveGlPrefix(commandName).Replace(
"AttribLFormat",
"AttributeDoubleFormat",
StringComparison.Ordinal));
}
if (commandName == "glVertexAttribIPointer")
{
return "VertexAttributeIntegerPointer";
}
if (commandName == "glVertexAttribLPointer")
{
return "VertexAttributeDoublePointer";
}
Match vertexMatch = _vertexAttributeModePattern.Match(commandName);
if (!vertexMatch.Success)
{
return null;
}
return vertexMatch.Groups["mode"].Value switch
{
"I" => "VertexAttributeInteger",
"L" => "VertexAttributeDouble",
"P" => $"VertexAttributePacked{ReadVertexAttributeWidth(commandName, 'P')}",
_ => null,
};
}
private static string ReadSemanticFamilyName(
string familyName
)
{
return familyName switch
{
"VertexAttrib" or "VertexAttribute" => "VertexAttribute",
"VertexAttrib4N" or "VertexAttribute4N" => "VertexAttributeNormalized",
"VertexAttribI" or "VertexAttributeI" => "VertexAttributeInteger",
"VertexAttribL" or "VertexAttributeL" => "VertexAttributeDouble",
"VertexAttribP" or "VertexAttributeP" => "VertexAttributePacked",
"GetVertexAttrib" or "GetVertexAttribute" => "GetVertexAttribute",
"GetVertexAttribI" or "GetVertexAttributeI" => "GetVertexAttributeInteger",
"GetnUniform" => "GetUniform",
"SamplerParameterI" => "SamplerParameterInteger",
"GetSamplerParameterI" => "GetSamplerParameterInteger",
"TexParameter" => "TextureParameter",
"TexParameterI" => "TextureParameterInteger",
"GetTexParameter" => "GetTextureParameter",
"GetTexParameterI" => "GetTextureParameterInteger",
"GetTexLevelParameter" => "GetTextureLevelParameter",
"TextureParameter" => "NamedTextureParameter",
"TextureParameterI" => "NamedTextureParameterInteger",
"GetTextureParameter" => "GetNamedTextureParameter",
"GetTextureParameterI" => "GetNamedTextureParameterInteger",
"GetTextureLevelParameter" => "GetNamedTextureLevelParameter",
_ => familyName,
};
}
private static string ReadDocumentationFamilyName(
string familyName
)
{
return familyName switch
{
"VertexAttrib4N" or "VertexAttribI" or "VertexAttribL" or
"VertexAttribP" => "VertexAttrib",
"GetVertexAttribI" => "GetVertexAttrib",
"SamplerParameterI" => "SamplerParameter",
"GetSamplerParameterI" => "GetSamplerParameter",
"TexParameterI" => "TexParameter",
"GetTexParameterI" => "GetTexParameter",
"TextureParameterI" => "TextureParameter",
"GetTextureParameterI" => "GetTextureParameter",
_ => familyName,
};
}
private static string? ReadSpecialDocumentationPage(
string commandName
)
{
if (commandName.StartsWith("glProgramUniformMatrix", StringComparison.Ordinal))
{
return "glProgramUniform";
}
if (commandName.StartsWith("glUniformMatrix", StringComparison.Ordinal))
{
return "glUniform";
}
if (commandName is "glVertexAttribIPointer" or "glVertexAttribLPointer")
{
return "glVertexAttribPointer";
}
if (commandName is "glVertexAttribIFormat" or "glVertexAttribLFormat" or
"glVertexArrayAttribFormat" or "glVertexArrayAttribIFormat" or
"glVertexArrayAttribLFormat")
{
return "glVertexAttribFormat";
}
if (commandName == "glClearBufferfi")
{
return "glClearBuffer";
}
if (commandName == "glClearNamedFramebufferfi")
{
return "glClearNamedFramebuffer";
}
if (commandName is "glGetBooleanv" or "glGetDoublev" or
"glGetFloatv" or "glGetIntegerv" or "glGetInteger64v" or
"glGetBooleani_v" or "glGetDoublei_v" or "glGetFloati_v" or
"glGetIntegeri_v" or "glGetInteger64i_v")
{
return "glGet";
}
if (commandName == "glGetVertexAttribPointerv")
{
return commandName;
}
if (commandName.StartsWith("glGetVertexAttrib", StringComparison.Ordinal))
{
return "glGetVertexAttrib";
}
if (commandName.StartsWith("glVertexAttrib", StringComparison.Ordinal) &&
commandName.Length > "glVertexAttrib".Length &&
char.IsDigit(commandName["glVertexAttrib".Length]) ||
commandName.StartsWith("glVertexAttribI", StringComparison.Ordinal) &&
commandName != "glVertexAttribIFormat" &&
commandName != "glVertexAttribIPointer" ||
commandName.StartsWith("glVertexAttribL", StringComparison.Ordinal) &&
commandName != "glVertexAttribLFormat" &&
commandName != "glVertexAttribLPointer" ||
commandName.StartsWith("glVertexAttribP", StringComparison.Ordinal))
{
return "glVertexAttrib";
}
return null;
}
private static char ReadVertexAttributeWidth(
string commandName,
char mode
)
{
int index = commandName.IndexOf(mode, "glVertexAttrib".Length) + 1;
return commandName[index];
}
private static string RemoveGlPrefix(
string name
)
{
return name.StartsWith("gl", StringComparison.Ordinal) && name.Length > 2
? name[2..]
: name;
}
private sealed record FamilyEntry(
GlFunctionFamily Family,
GlFunctionNameShape? Shape
);
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<DisablePostBuildCopy>true</DisablePostBuildCopy>
<DisableNativeCopy>true</DisableNativeCopy>
<!-- CLI output is a stable developer-facing protocol, not localized UI. -->
<NoWarn>$(NoWarn);CA1303</NoWarn>
</PropertyGroup>
<ItemGroup>
<None Update="xml\gl.xml"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project>
+60
View File
@@ -0,0 +1,60 @@
// -----------------------------------------------------------------------------
// file="Program"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.IO;
using System.Xml;
namespace StgSharp.GenerateGL
{
internal static class Program
{
private static int Main(
string[] args
)
{
try
{
return XmlCli.Run(args);
}
catch (IOException exception)
{
return WriteError(exception);
}
catch (XmlException exception)
{
return WriteError(exception);
}
catch (InvalidDataException exception)
{
return WriteError(exception);
}
catch (UnauthorizedAccessException exception)
{
return WriteError(exception);
}
catch (ArgumentException exception)
{
return WriteError(exception);
}
catch (NotSupportedException exception)
{
return WriteError(exception);
}
}
private static int WriteError(
Exception exception
)
{
Console.Error.WriteLine($"error: {exception.Message}");
return XmlCli.LoadErrorExitCode;
}
}
}
+180
View File
@@ -0,0 +1,180 @@
# StgSharp.GenerateGL
`StgSharp.GenerateGL` generates the StgSharp desktop OpenGL C# bindings from the Khronos `gl.xml` registry.
The current target is OpenGL 4.6 Core. The generator does not emit OpenGL ES or mix extension entry points into the core API. `gl.xml` is the authoritative generation input; the retained `gl.h` is reference material only.
## Usage
Start the interactive CLI:
```powershell
dotnet run --project tools/Nitload.GenerateGL/Nitload.GenerateGL.csproj
```
Common commands:
```text
summary
find glBindBuffer
show command glBindBuffer
target summary
families list Vertex
families show VertexAttributeP
generate all
list files
preview OpenGLFunction.v4_1.g.cs
```
Generate directly into an existing directory:
```powershell
dotnet run --project tools/Nitload.GenerateGL/Nitload.GenerateGL.csproj -- generate all src/Nitload.Graphics/OpenGL/Generated
```
Generation targets are explicit:
```text
generate api [output-directory] # function tables and managed forwarding APIs
generate enum [output-directory] # glConst and strongly typed enum groups
generate all [output-directory] # both sets
```
The API files reference the generated enum types. Use `generate all` for a clean output
directory; use `generate api` when refreshing an output tree that already contains the enum
files.
Without a directory, `generate` retains its output in the current CLI session. With a directory:
- the directory must already exist and be writable;
- each file is staged in the destination directory before replacing its target;
- an existing file is replaced only when its first line is `// <auto-generated>`;
- unrelated and obsolete generated files are not deleted.
Use a directory dedicated to generated source files.
## Pipeline
```text
gl.xml
-> lossless XML and registry models
-> desktop OpenGL 4.6 Core projection
-> GLenum/GLbitfield semantic group projection
-> function-family and scalar/vector relation analysis
-> managed API filters
-> C# naming and signature validation
-> in-memory generated file set
-> preview or directory output
```
The target projection removes GLES and ARB/NV/EXT extension entry points. It retains only commands and constants present in the target Core Profile.
Generated files:
- `glconst.cs`: core constants;
- `GlEnums.g.cs`: strongly typed enum groups referenced by Core Profile parameters and return values;
- `OpenglContext.g.cs`: the function-pointer table with native names and ABI types;
- `glContextShadow.g.cs`: an identically ordered `nint` table used while loading native entry points;
- `OpenGLFunction.Base.g.cs`: APIs introduced through OpenGL 3.2 and retained in the target profile;
- `OpenGLFunction.v3_3.g.cs` through `OpenGLFunction.v4_6.g.cs`: APIs introduced by each later core version.
`OpenglContext` intentionally remains close to the C ABI. Overloads, `Span<T>`, `GlHandle`, and managed naming exist only in the `OpenGLFunction` layer.
## Overload rules
The generator merges a family only when it can validate the resulting signatures. Every managed method retains a reference to its original `gl*` command.
| Native C API | Generated API |
| --- | --- |
| `glUniform1f` through `glUniform4f` | Scalar `Uniform(...)` overloads |
| `glUniform*i`, `glUniform*ui`, and `glUniform*d` | Merged into `Uniform(...)` by C# parameter type |
| `glProgramUniform*` | Merged into `ProgramUniform(...)` by the same rules |
| `glUniform1*v` | `Uniform(..., ReadOnlySpan<T>)` |
| `glUniform4*v` | `Uniform(..., ReadOnlySpan<Vec4<T>>)` |
| `glUniform2*v` and `glUniform3*v` | Omitted because `Vec2` and `Vec3` storage is not a tightly packed C array |
| `glUniformMatrix4fv` | `Uniform(..., GraphicsMatrix)` and matrix-span overloads |
| Other matrix sizes and double matrices | Omitted because only the graphics-oriented 4x4 float layout is defined |
| `glGetBooleanv`, `glGetFloatv`, `glGetIntegerv`, and related queries | `GetValue(..., Span<T>)` |
| `glVertexAttrib1f/2f/3f/4f` and equivalent scalar forms | `VertexAttribute(...)`; component count is expressed by the parameter list |
| `glVertexAttrib*fv/*iv/...` with a scalar equivalent | Omitted instead of exposing a duplicate C-style pointer entry point |
| `glVertexAttrib4N*` | `VertexAttributeNormalized(...)` |
| `glVertexAttribI*` | `VertexAttributeInteger(...)` |
| `glVertexAttribL*` | `VertexAttributeDouble(...)`; `L` does not mean C# `long` |
| `glVertexAttribP1ui` through `P4ui` | `VertexAttributePacked1(...)` through `Packed4(...)`; the width prevents signature collisions |
| `glGetVertexAttribI*` and `L*` | `GetVertexAttributeInteger(...)` and `GetVertexAttributeDouble(...)` |
| `glScissorIndexed` and `glScissorIndexedv` | `ScissorIndexed(...)` overloads |
Function families are inferred from common name prefixes, shape suffixes, type suffixes, and vector forms. Candidates are checked against:
- the `gl.xml` `<vecequiv>` relation;
- parameter `len`, `kind`, and native types;
- scalar `T` versus `const T*` relationships;
- uniqueness of the final C# signature.
If incomplete type mapping causes two methods to collapse to the same C# signature, the generator conservatively restores their normalized native names instead of emitting uncompilable overloads.
## Renaming rules
Safe lexical normalization applies to all public methods:
| OpenGL spelling | C# spelling |
| --- | --- |
| `Framebuffer` / `framebuffer` | `FrameBuffer` / `frameBuffer` |
| `Renderbuffer` / `renderbuffer` | `RenderBuffer` / `renderBuffer` |
| `Internalformat` / `internalformat` | `InternalFormat` / `internalFormat` |
| `Attrib` / `attrib` | `Attribute` / `attribute` |
| `Frag` | `Fragment` |
| `attribindex` | `attributeIndex` |
| `bindingindex` | `bindingIndex` |
| `relativeoffset` | `relativeOffset` |
| `pname` | `pName` |
| `xoffset` / `yoffset` / `zoffset` | `xOffset` / `yOffset` / `zOffset` |
| `vaobj` | `vertexArray` |
Families that represent distinct semantics receive explicit names:
| Native family | Managed family |
| --- | --- |
| `TexParameter*` | `TextureParameter(...)` |
| DSA `TextureParameter*` | `NamedTextureParameter(...)` |
| `TexParameterI*` | `TextureParameterInteger(...)` |
| DSA `TextureParameterI*` | `NamedTextureParameterInteger(...)` |
| `SamplerParameterI*` | `SamplerParameterInteger(...)` |
| `GetnUniform*` | Merged into `GetUniform(...)` when signatures permit |
| `VertexAttribI/L/P/N` | Expanded to `Integer/Double/Packed/Normalized` |
Renaming never changes the native target. For example, `VertexAttributeInteger(...)` still calls `__context->glVertexAttribI*`.
## Type rules
- `gl.xml` typedefs map to equivalent C# ABI types; the generator does not emit separate type aliases;
- `GLenum` and `GLbitfield` declarations with one unambiguous registry group use the generated enum type in public APIs;
- enum pointers preserve their pointer shape (for example, `GLenum*` becomes `DebugSource*`) and are cast back to the native `uint*` representation at the forwarding boundary;
- enum return values are converted from the native `uint` representation before being returned;
- declarations with missing or conflicting enum group metadata remain on their ABI type;
- a `GLuint` is converted to `GlHandle` only when object class and parameter semantics identify it as an object handle;
- handle arrays use `Span<GlHandle>` or `ReadOnlySpan<GlHandle>`;
- `OpenGLFunction` stores an `OpenglContext*` and forwards through `__context->glFunction(...)`;
- unknown-length arrays, complex outputs, multiple indirection, and `void*` remain pointers.
A `T*` alone does not determine whether a managed API should use `Span<T>`, `ReadOnlySpan<T>`, `out T`, or a return value. Future pointer improvements should be implemented as family-specific filters validated with `len`, parameter direction, and OpenGL state semantics.
Raw managed forwards are omitted when a safer handwritten public API owns the command. In particular, `glShaderSource` remains in `OpenglContext` for ABI loading, while `OpenGLFunction` exposes the handwritten `ReadOnlySpan<byte>` and `string` overloads instead of a generated `byte**` signature.
## Documentation comments
Every generated public method contains:
- its original C API declaration;
- a `<seealso>` link to the corresponding docs.gl OpenGL 4 page or family page.
The generator does not emit `<param>` or `<returns>` elements and does not copy docs.gl content.
## Source layout
- `Registry/`: the `gl.xml` object model and Core Profile projection;
- `Analysis/`: function families, name shapes, and scalar/vector relations;
- `Generation/`: type mapping, API filters, signature planning, and file generation;
- `Naming/`: C# identifier and semantic-family naming policies;
- `XmlCli.cs`, `RegistryCli.cs`, and `FunctionFamilyCli.cs`: interactive inspection;
- `GenerateCli.cs`: in-memory preview and protected directory output.
@@ -0,0 +1,258 @@
// -----------------------------------------------------------------------------
// file="DesktopGlCoreRegistryFilter"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace StgSharp.GenerateGL.Registry
{
/// <summary>
/// Selects the active desktop OpenGL core API without extension requirements.
/// </summary>
internal static class DesktopGlCoreRegistryFilter
{
public const string ApiName = "gl";
public const string CoreProfile = "core";
public const string LatestVersion = "4.6";
public static GlRegistryProjection Apply(
GlRegistryModel registry,
string maximumVersion = LatestVersion
)
{
ArgumentNullException.ThrowIfNull(registry);
Version maximum = ReadVersion(maximumVersion);
GlFeatureDefinition[] features = registry.Features
.Where(
value => string.Equals(value.Api, ApiName, StringComparison.Ordinal) &&
ReadVersion(value.Number) <= maximum)
.ToArray();
Dictionary<string, GlCommandDefinition> commandDefinitions = ReadCommandDefinitions(
registry);
Dictionary<string, GlEnumDefinition> enumDefinitions = ReadEnumDefinitions(registry);
SymbolSelection commands = new SymbolSelection();
SymbolSelection enums = new SymbolSelection();
foreach (GlFeatureDefinition feature in features)
{
foreach (GlRequirementBlock block in feature.Requirements.Where(AppliesToTarget))
{
foreach (GlRegistryReference reference in block.References)
{
SymbolSelection? selection = reference.Kind switch
{
GlRegistryReferenceKind.Command => commands,
GlRegistryReferenceKind.Enum => enums,
GlRegistryReferenceKind.Type => null,
_ => throw new InvalidDataException(
$"Unknown registry reference kind '{reference.Kind}'."),
};
if (selection is null)
{
continue;
}
if (block.Operation == GlRequirementOperation.Require)
{
selection.Require(reference.Name, feature.Number);
}
else
{
selection.Remove(reference.Name);
}
}
}
}
GlCommandDefinition[] selectedCommands = commands.ReadActiveNames()
.Select(
name => ReadDefinition(
commandDefinitions,
name,
"command"))
.ToArray();
GlEnumDefinition[] selectedEnums = enums.ReadActiveNames()
.Select(
name => ReadDefinition(
enumDefinitions,
name,
"enum"))
.ToArray();
return new GlRegistryProjection(
registry,
ApiName,
CoreProfile,
maximumVersion,
Freeze(features),
Freeze(selectedCommands),
Freeze(selectedEnums),
commands.FreezeIntroducedVersions(),
enums.FreezeIntroducedVersions());
}
private static bool AppliesToTarget(
GlRequirementBlock block
)
{
bool matchesApi = block.Api is null || string.Equals(
block.Api,
ApiName,
StringComparison.Ordinal);
bool matchesProfile = block.Profile is null || string.Equals(
block.Profile,
CoreProfile,
StringComparison.Ordinal);
return matchesApi && matchesProfile;
}
private static Dictionary<string, GlCommandDefinition> ReadCommandDefinitions(
GlRegistryModel registry
)
{
Dictionary<string, GlCommandDefinition> result = new Dictionary<string, GlCommandDefinition>(
StringComparer.Ordinal);
foreach (GlCommandDefinition command in registry.Commands)
{
if (!result.TryAdd(command.Name, command))
{
throw new InvalidDataException(
$"Command '{command.Name}' has more than one definition.");
}
}
return result;
}
private static Dictionary<string, GlEnumDefinition> ReadEnumDefinitions(
GlRegistryModel registry
)
{
Dictionary<string, GlEnumDefinition> result = new Dictionary<string, GlEnumDefinition>(
StringComparer.Ordinal);
foreach (GlEnumDefinition value in registry.EnumBlocks.SelectMany(
block => block.Enums).Where(IsDesktopDefinition))
{
if (!result.TryGetValue(value.Name, out GlEnumDefinition? previous))
{
result.Add(value.Name, value);
continue;
}
if (previous.Value is not null && value.Value is not null && !string.Equals(
previous.Value,
value.Value,
StringComparison.Ordinal))
{
throw new InvalidDataException(
$"Enum '{value.Name}' has conflicting desktop values " +
$"'{previous.Value}' and '{value.Value}'.");
}
if (previous.Value is null && value.Value is not null)
{
result[value.Name] = value;
}
}
return result;
}
private static bool IsDesktopDefinition(
GlEnumDefinition value
)
{
return value.Api is null || string.Equals(
value.Api,
ApiName,
StringComparison.Ordinal);
}
private static T ReadDefinition<T>(
IReadOnlyDictionary<string, T> definitions,
string name,
string kind
)
{
if (!definitions.TryGetValue(name, out T? definition))
{
throw new InvalidDataException(
$"Desktop OpenGL core references undefined {kind} '{name}'.");
}
return definition;
}
private static Version ReadVersion(
string value
)
{
if (!Version.TryParse(value, out Version? version))
{
throw new InvalidDataException(
$"OpenGL feature version '{value}' is not a valid version.");
}
return version;
}
private static ReadOnlyCollection<T> Freeze<T>(
IEnumerable<T> values
)
{
return Array.AsReadOnly(values.ToArray());
}
private sealed class SymbolSelection
{
private readonly HashSet<string> _active = new HashSet<string>(StringComparer.Ordinal);
private readonly Dictionary<string, string> _introducedVersions = new Dictionary<string, string>(
StringComparer.Ordinal);
private readonly List<string> _order = new List<string>();
public void Require(
string name,
string version
)
{
_active.Add(name);
if (_introducedVersions.TryAdd(name, version))
{
_order.Add(name);
}
}
public void Remove(
string name
)
{
_active.Remove(name);
}
public IEnumerable<string> ReadActiveNames()
{
return _order.Where(_active.Contains);
}
public ReadOnlyDictionary<string, string> FreezeIntroducedVersions()
{
return new ReadOnlyDictionary<string, string>(
new Dictionary<string, string>(
_introducedVersions,
StringComparer.Ordinal));
}
}
}
}
@@ -0,0 +1,59 @@
// -----------------------------------------------------------------------------
// file="GlApiDefinition"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
namespace StgSharp.GenerateGL.Registry
{
internal sealed record GlFeatureDefinition(
string Name,
string Api,
string Number,
string? Protect,
string? Comment,
IReadOnlyList<GlRequirementBlock> Requirements,
GlRegistryElement Element
);
internal sealed record GlExtensionDefinition(
string Name,
string? SupportedApis,
string? Protect,
string? Comment,
IReadOnlyList<GlRequirementBlock> Requirements,
GlRegistryElement Element
);
internal sealed record GlRequirementBlock(
GlRequirementOperation Operation,
string? Api,
string? Profile,
string? Comment,
IReadOnlyList<GlRegistryReference> References,
GlRegistryElement Element
);
internal sealed record GlRegistryReference(
GlRegistryReferenceKind Kind,
string Name,
string? Comment,
GlRegistryElement Element
);
internal enum GlRequirementOperation
{
Require,
Remove,
}
internal enum GlRegistryReferenceKind
{
Type,
Enum,
Command,
}
}
@@ -0,0 +1,56 @@
// -----------------------------------------------------------------------------
// file="GlCommandDefinition"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
namespace StgSharp.GenerateGL.Registry
{
internal sealed record GlCommandBlock(
string? Namespace,
IReadOnlyList<GlCommandDefinition> Commands,
GlRegistryElement Element
);
internal sealed record GlCommandDefinition(
string Name,
GlReturnDefinition Return,
IReadOnlyList<GlParameterDefinition> Parameters,
IReadOnlyList<GlNamedReference> Aliases,
IReadOnlyList<GlNamedReference> VectorEquivalents,
IReadOnlyList<GlCommandProtocol> Protocols,
string? Comment,
GlRegistryElement Element
);
internal sealed record GlReturnDefinition(
string DeclarationText,
string? TypeName,
string? Group,
string? Kind,
string? ObjectClass,
GlRegistryElement Element
);
internal sealed record GlParameterDefinition(
string Name,
string DeclarationText,
string? TypeName,
string? Group,
string? Kind,
string? ObjectClass,
string? Length,
GlRegistryElement Element
);
internal sealed record GlCommandProtocol(
string Type,
string Opcode,
string? Name,
string? Comment,
GlRegistryElement Element
);
}
@@ -0,0 +1,50 @@
// -----------------------------------------------------------------------------
// file="GlEnumDefinition"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
namespace StgSharp.GenerateGL.Registry
{
internal sealed record GlEnumDefinition(
string Name,
string? Value,
string? Api,
string? TypeSuffix,
string? Group,
string? Alias,
string? Comment,
GlRegistryElement Element
);
internal sealed record GlEnumBlock(
string? Namespace,
string? Group,
string? Type,
string? Start,
string? End,
string? Vendor,
string? Comment,
IReadOnlyList<GlEnumDefinition> Enums,
IReadOnlyList<GlUnusedEnumRange> UnusedRanges,
GlRegistryElement Element
);
internal sealed record GlEnumGroup(
string Name,
string? Comment,
IReadOnlyList<GlNamedReference> Members,
GlRegistryElement Element
);
internal sealed record GlUnusedEnumRange(
string Start,
string? End,
string? Vendor,
string? Comment,
GlRegistryElement Element
);
}
@@ -0,0 +1,30 @@
// -----------------------------------------------------------------------------
// file="GlRegistryElement"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
namespace StgSharp.GenerateGL.Registry
{
/// <summary>
/// Preserves the XML representation shared by every registry model item.
/// </summary>
internal sealed record GlRegistryElement(
GlSourceLocation Location,
string RawXml,
IReadOnlyDictionary<string, string> Attributes
);
internal readonly record struct GlSourceLocation(
int LineNumber,
int LinePosition
);
internal sealed record GlNamedReference(
string Name,
GlRegistryElement Element
);
}
@@ -0,0 +1,126 @@
// -----------------------------------------------------------------------------
// file="GlRegistryModel"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace StgSharp.GenerateGL.Registry
{
/// <summary>
/// Contains the structural OpenGL registry model without managed type mapping.
/// </summary>
internal sealed class GlRegistryModel
{
public GlRegistryModel(
string sourcePath,
string? comment,
IEnumerable<GlTypeDefinition> types,
IEnumerable<GlKindDefinition> kinds,
IEnumerable<GlEnumGroup> enumGroups,
IEnumerable<GlEnumBlock> enumBlocks,
IEnumerable<GlCommandBlock> commandBlocks,
IEnumerable<GlFeatureDefinition> features,
IEnumerable<GlExtensionDefinition> extensions,
GlRegistryElement element
)
{
ArgumentException.ThrowIfNullOrWhiteSpace(sourcePath);
ArgumentNullException.ThrowIfNull(types);
ArgumentNullException.ThrowIfNull(kinds);
ArgumentNullException.ThrowIfNull(enumGroups);
ArgumentNullException.ThrowIfNull(enumBlocks);
ArgumentNullException.ThrowIfNull(commandBlocks);
ArgumentNullException.ThrowIfNull(features);
ArgumentNullException.ThrowIfNull(extensions);
ArgumentNullException.ThrowIfNull(element);
SourcePath = sourcePath;
Comment = comment;
Types = Freeze(types);
Kinds = Freeze(kinds);
EnumGroups = Freeze(enumGroups);
EnumBlocks = Freeze(enumBlocks);
CommandBlocks = Freeze(commandBlocks);
Commands = Freeze(CommandBlocks.SelectMany(value => value.Commands));
Features = Freeze(features);
Extensions = Freeze(extensions);
Element = element;
TypesByName = CreateIndex(Types, value => value.Name);
KindsByName = CreateIndex(Kinds, value => value.Name);
EnumGroupsByName = CreateIndex(EnumGroups, value => value.Name);
EnumsByName = CreateIndex(
EnumBlocks.SelectMany(value => value.Enums),
value => value.Name);
CommandsByName = CreateIndex(Commands, value => value.Name);
FeaturesByName = CreateIndex(Features, value => value.Name);
ExtensionsByName = CreateIndex(Extensions, value => value.Name);
}
public string SourcePath { get; }
public string? Comment { get; }
public IReadOnlyList<GlTypeDefinition> Types { get; }
public IReadOnlyList<GlKindDefinition> Kinds { get; }
public IReadOnlyList<GlEnumGroup> EnumGroups { get; }
public IReadOnlyList<GlEnumBlock> EnumBlocks { get; }
public IReadOnlyList<GlCommandBlock> CommandBlocks { get; }
public IReadOnlyList<GlCommandDefinition> Commands { get; }
public IReadOnlyList<GlFeatureDefinition> Features { get; }
public IReadOnlyList<GlExtensionDefinition> Extensions { get; }
public GlRegistryElement Element { get; }
public IReadOnlyDictionary<string, IReadOnlyList<GlTypeDefinition>> TypesByName { get; }
public IReadOnlyDictionary<string, IReadOnlyList<GlKindDefinition>> KindsByName { get; }
public IReadOnlyDictionary<string, IReadOnlyList<GlEnumGroup>> EnumGroupsByName { get; }
public IReadOnlyDictionary<string, IReadOnlyList<GlEnumDefinition>> EnumsByName { get; }
public IReadOnlyDictionary<string, IReadOnlyList<GlCommandDefinition>> CommandsByName { get; }
public IReadOnlyDictionary<string, IReadOnlyList<GlFeatureDefinition>> FeaturesByName { get; }
public IReadOnlyDictionary<string, IReadOnlyList<GlExtensionDefinition>> ExtensionsByName { get; }
private static ReadOnlyDictionary<string, IReadOnlyList<T>> CreateIndex<T>(
IEnumerable<T> values,
Func<T, string> readName
)
{
Dictionary<string, IReadOnlyList<T>> index = values.GroupBy(
readName,
StringComparer.Ordinal)
.ToDictionary(
value => value.Key,
value => (IReadOnlyList<T>)Freeze(value),
StringComparer.Ordinal);
return new ReadOnlyDictionary<string, IReadOnlyList<T>>(index);
}
private static ReadOnlyCollection<T> Freeze<T>(
IEnumerable<T> values
)
{
return Array.AsReadOnly(values.ToArray());
}
}
}
@@ -0,0 +1,26 @@
// -----------------------------------------------------------------------------
// file="GlRegistryProjection"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
namespace StgSharp.GenerateGL.Registry
{
/// <summary>
/// Represents one API/profile target selected from the complete registry.
/// </summary>
internal sealed record GlRegistryProjection(
GlRegistryModel Source,
string Api,
string Profile,
string MaximumVersion,
IReadOnlyList<GlFeatureDefinition> Features,
IReadOnlyList<GlCommandDefinition> Commands,
IReadOnlyList<GlEnumDefinition> Enums,
IReadOnlyDictionary<string, string> CommandIntroducedVersions,
IReadOnlyDictionary<string, string> EnumIntroducedVersions
);
}
@@ -0,0 +1,511 @@
// -----------------------------------------------------------------------------
// file="GlRegistryReader"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using StgSharp.GenerateGL.Tree;
namespace StgSharp.GenerateGL.Registry
{
/// <summary>
/// Projects the generic XML tree into the structural OpenGL registry model.
/// </summary>
internal static class GlRegistryReader
{
private static readonly IReadOnlyDictionary<string, string> _emptyAttributes =
new ReadOnlyDictionary<string, string>(
new Dictionary<string, string>(StringComparer.Ordinal));
public static GlRegistryModel Read(
XmlTreeDocument document
)
{
ArgumentNullException.ThrowIfNull(document);
XElement root = document.Tree.Root ?? throw new InvalidDataException(
$"XML document '{document.SourcePath}' does not contain a root element.");
if (root.Name != "registry")
{
throw CreateError(
document,
root,
$"Expected <registry>, but found <{root.Name}>.");
}
ValidateChildren(
document,
root,
"comment",
"types",
"kinds",
"groups",
"enums",
"commands",
"feature",
"extensions");
return new GlRegistryModel(
document.SourcePath,
ReadOptionalElementText(root, "comment"),
ReadSection(document, root, "types", "type").Select(
value => ReadType(document, value)),
ReadSection(document, root, "kinds", "kind").Select(
value => ReadKind(document, value)),
ReadSection(document, root, "groups", "group").Select(
value => ReadEnumGroup(document, value)),
root.Elements("enums").Select(value => ReadEnumBlock(document, value)),
root.Elements("commands").Select(
value => ReadCommandBlock(document, value)),
root.Elements("feature").Select(value => ReadFeature(document, value)),
ReadSection(document, root, "extensions", "extension").Select(
value => ReadExtension(document, value)),
ReadElement(root));
}
private static GlTypeDefinition ReadType(
XmlTreeDocument document,
XElement element
)
{
string name = ReadOptionalAttribute(element, "name") ??
ReadOptionalElementText(element, "name") ??
throw CreateError(
document,
element,
"A registry <type> requires a name attribute or child element.");
return new GlTypeDefinition(
name,
ReadDeclarationText(element),
ReadOptionalAttribute(element, "api"),
ReadOptionalAttribute(element, "requires"),
ReadOptionalAttribute(element, "type"),
ReadOptionalAttribute(element, "comment"),
ReadElement(element));
}
private static GlKindDefinition ReadKind(
XmlTreeDocument document,
XElement element
)
{
return new GlKindDefinition(
ReadRequiredAttribute(document, element, "name"),
ReadRequiredAttribute(document, element, "desc"),
ReadElement(element));
}
private static GlEnumGroup ReadEnumGroup(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(document, element, "enum");
return new GlEnumGroup(
ReadRequiredAttribute(document, element, "name"),
ReadOptionalAttribute(element, "comment"),
Freeze(
element.Elements("enum").Select(
value => new GlNamedReference(
ReadRequiredAttribute(document, value, "name"),
ReadElement(value)))),
ReadElement(element));
}
private static GlEnumBlock ReadEnumBlock(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(document, element, "enum", "unused");
return new GlEnumBlock(
ReadOptionalAttribute(element, "namespace"),
ReadOptionalAttribute(element, "group"),
ReadOptionalAttribute(element, "type"),
ReadOptionalAttribute(element, "start"),
ReadOptionalAttribute(element, "end"),
ReadOptionalAttribute(element, "vendor"),
ReadOptionalAttribute(element, "comment"),
Freeze(
element.Elements("enum").Select(
value => ReadEnum(document, value))),
Freeze(
element.Elements("unused").Select(
value => ReadUnusedEnumRange(document, value))),
ReadElement(element));
}
private static GlEnumDefinition ReadEnum(
XmlTreeDocument document,
XElement element
)
{
return new GlEnumDefinition(
ReadRequiredAttribute(document, element, "name"),
ReadOptionalAttribute(element, "value"),
ReadOptionalAttribute(element, "api"),
ReadOptionalAttribute(element, "type"),
ReadOptionalAttribute(element, "group"),
ReadOptionalAttribute(element, "alias"),
ReadOptionalAttribute(element, "comment"),
ReadElement(element));
}
private static GlUnusedEnumRange ReadUnusedEnumRange(
XmlTreeDocument document,
XElement element
)
{
return new GlUnusedEnumRange(
ReadRequiredAttribute(document, element, "start"),
ReadOptionalAttribute(element, "end"),
ReadOptionalAttribute(element, "vendor"),
ReadOptionalAttribute(element, "comment"),
ReadElement(element));
}
private static GlCommandBlock ReadCommandBlock(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(document, element, "command");
return new GlCommandBlock(
ReadOptionalAttribute(element, "namespace"),
Freeze(
element.Elements("command").Select(
value => ReadCommand(document, value))),
ReadElement(element));
}
private static GlCommandDefinition ReadCommand(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(
document,
element,
"proto",
"param",
"alias",
"vecequiv",
"glx");
XElement prototype = element.Element("proto") ?? throw CreateError(
document,
element,
"A registry <command> requires a <proto> child element.");
string name = ReadRequiredElementText(document, prototype, "name");
return new GlCommandDefinition(
name,
ReadReturn(document, prototype),
Freeze(
element.Elements("param").Select(
value => ReadParameter(document, value))),
Freeze(
element.Elements("alias").Select(
value => ReadNamedReference(document, value))),
Freeze(
element.Elements("vecequiv").Select(
value => ReadNamedReference(document, value))),
Freeze(
element.Elements("glx").Select(
value => ReadCommandProtocol(document, value))),
ReadOptionalAttribute(element, "comment"),
ReadElement(element));
}
private static GlReturnDefinition ReadReturn(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(document, element, "ptype", "name");
_ = ReadRequiredElementText(document, element, "name");
return new GlReturnDefinition(
ReadDeclarationText(element),
ReadOptionalElementText(element, "ptype"),
ReadOptionalAttribute(element, "group"),
ReadOptionalAttribute(element, "kind"),
ReadOptionalAttribute(element, "class"),
ReadElement(element));
}
private static GlParameterDefinition ReadParameter(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(document, element, "ptype", "name");
return new GlParameterDefinition(
ReadRequiredElementText(document, element, "name"),
ReadDeclarationText(element),
ReadOptionalElementText(element, "ptype"),
ReadOptionalAttribute(element, "group"),
ReadOptionalAttribute(element, "kind"),
ReadOptionalAttribute(element, "class"),
ReadOptionalAttribute(element, "len"),
ReadElement(element));
}
private static GlNamedReference ReadNamedReference(
XmlTreeDocument document,
XElement element
)
{
return new GlNamedReference(
ReadRequiredAttribute(document, element, "name"),
ReadElement(element));
}
private static GlCommandProtocol ReadCommandProtocol(
XmlTreeDocument document,
XElement element
)
{
return new GlCommandProtocol(
ReadRequiredAttribute(document, element, "type"),
ReadRequiredAttribute(document, element, "opcode"),
ReadOptionalAttribute(element, "name"),
ReadOptionalAttribute(element, "comment"),
ReadElement(element));
}
private static GlFeatureDefinition ReadFeature(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(document, element, "require", "remove");
return new GlFeatureDefinition(
ReadRequiredAttribute(document, element, "name"),
ReadRequiredAttribute(document, element, "api"),
ReadRequiredAttribute(document, element, "number"),
ReadOptionalAttribute(element, "protect"),
ReadOptionalAttribute(element, "comment"),
Freeze(
element.Elements().Select(
value => ReadRequirement(document, value))),
ReadElement(element));
}
private static GlExtensionDefinition ReadExtension(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(document, element, "require", "remove");
return new GlExtensionDefinition(
ReadRequiredAttribute(document, element, "name"),
ReadOptionalAttribute(element, "supported"),
ReadOptionalAttribute(element, "protect"),
ReadOptionalAttribute(element, "comment"),
Freeze(
element.Elements().Select(
value => ReadRequirement(document, value))),
ReadElement(element));
}
private static GlRequirementBlock ReadRequirement(
XmlTreeDocument document,
XElement element
)
{
ValidateChildren(document, element, "type", "enum", "command");
GlRequirementOperation operation = element.Name.LocalName switch
{
"require" => GlRequirementOperation.Require,
"remove" => GlRequirementOperation.Remove,
_ => throw CreateError(
document,
element,
$"Expected <require> or <remove>, but found <{element.Name}>."),
};
return new GlRequirementBlock(
operation,
ReadOptionalAttribute(element, "api"),
ReadOptionalAttribute(element, "profile"),
ReadOptionalAttribute(element, "comment"),
Freeze(
element.Elements().Select(
value => ReadRegistryReference(document, value))),
ReadElement(element));
}
private static GlRegistryReference ReadRegistryReference(
XmlTreeDocument document,
XElement element
)
{
GlRegistryReferenceKind kind = element.Name.LocalName switch
{
"type" => GlRegistryReferenceKind.Type,
"enum" => GlRegistryReferenceKind.Enum,
"command" => GlRegistryReferenceKind.Command,
_ => throw CreateError(
document,
element,
$"Unsupported registry reference <{element.Name}>."),
};
return new GlRegistryReference(
kind,
ReadRequiredAttribute(document, element, "name"),
ReadOptionalAttribute(element, "comment"),
ReadElement(element));
}
private static IEnumerable<XElement> ReadSection(
XmlTreeDocument document,
XElement root,
XName sectionName,
XName itemName
)
{
foreach (XElement section in root.Elements(sectionName))
{
ValidateChildren(document, section, itemName.LocalName);
foreach (XElement item in section.Elements(itemName))
{
yield return item;
}
}
}
private static string ReadRequiredAttribute(
XmlTreeDocument document,
XElement element,
XName name
)
{
string? value = ReadOptionalAttribute(element, name);
if (value is null)
{
throw CreateError(
document,
element,
$"<{element.Name}> requires the '{name}' attribute.");
}
return value;
}
private static string? ReadOptionalAttribute(
XElement element,
XName name
)
{
string? value = (string?)element.Attribute(name);
return string.IsNullOrWhiteSpace(value) ? null : value;
}
private static string ReadRequiredElementText(
XmlTreeDocument document,
XElement element,
XName name
)
{
string? value = ReadOptionalElementText(element, name);
if (value is null)
{
throw CreateError(
document,
element,
$"<{element.Name}> requires a non-empty <{name}> child element.");
}
return value;
}
private static string? ReadOptionalElementText(
XElement element,
XName name
)
{
string? value = element.Element(name)?.Value.Trim();
return string.IsNullOrWhiteSpace(value) ? null : value;
}
private static string ReadDeclarationText(
XElement element
)
{
return element.Value.Trim();
}
private static GlRegistryElement ReadElement(
XElement element
)
{
IXmlLineInfo lineInfo = element;
GlSourceLocation location = lineInfo.HasLineInfo()
? new GlSourceLocation(lineInfo.LineNumber, lineInfo.LinePosition)
: default;
IReadOnlyDictionary<string, string> attributes = element.HasAttributes
? new ReadOnlyDictionary<string, string>(
element.Attributes().ToDictionary(
value => value.Name.ToString(),
value => value.Value,
StringComparer.Ordinal))
: _emptyAttributes;
return new GlRegistryElement(
location,
element.ToString(SaveOptions.DisableFormatting),
attributes);
}
private static ReadOnlyCollection<T> Freeze<T>(
IEnumerable<T> values
)
{
return Array.AsReadOnly(values.ToArray());
}
private static void ValidateChildren(
XmlTreeDocument document,
XElement element,
params string[] allowedNames
)
{
HashSet<string> allowed = new HashSet<string>(
allowedNames,
StringComparer.Ordinal);
XElement? unsupported = element.Elements().FirstOrDefault(
value => !allowed.Contains(value.Name.LocalName));
if (unsupported is not null)
{
throw CreateError(
document,
unsupported,
$"Unsupported <{unsupported.Name}> child in <{element.Name}>.");
}
}
private static InvalidDataException CreateError(
XmlTreeDocument document,
XElement element,
string message
)
{
IXmlLineInfo lineInfo = element;
string location = lineInfo.HasLineInfo()
? $"({lineInfo.LineNumber},{lineInfo.LinePosition})"
: string.Empty;
return new InvalidDataException(
$"{document.SourcePath}{location}: {message}");
}
}
}
@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------------
// file="GlTypeDefinition"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
namespace StgSharp.GenerateGL.Registry
{
internal sealed record GlTypeDefinition(
string Name,
string DeclarationText,
string? Api,
string? RequiredType,
string? Category,
string? Comment,
GlRegistryElement Element
);
internal sealed record GlKindDefinition(
string Name,
string Description,
GlRegistryElement Element
);
}
+862
View File
@@ -0,0 +1,862 @@
// -----------------------------------------------------------------------------
// file="RegistryCli"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL
{
/// <summary>
/// Presents the parsed registry model as the primary CLI surface.
/// </summary>
internal static class RegistryCli
{
private const int ShowAllResults = int.MaxValue;
private const int QueryErrorExitCode = 1;
private const int SuccessExitCode = 0;
private const int UsageErrorExitCode = 2;
public static int Execute(
GlRegistryModel registry,
string command,
IReadOnlyList<string> arguments
)
{
ArgumentNullException.ThrowIfNull(registry);
return command switch
{
"SUMMARY" or "REGISTRY" or "MODEL" => QuerySummary(
registry,
arguments),
"LIST" or "LS" => QueryList(registry, arguments),
"FIND" or "SEARCH" => QueryFind(registry, arguments),
"SHOW" or "INFO" => QueryShow(registry, arguments),
_ => WriteUsageError($"Unknown registry command '{command}'."),
};
}
private static int QuerySummary(
GlRegistryModel registry,
IReadOnlyCollection<string> arguments
)
{
if (!ExpectNoArguments("summary", arguments))
{
return UsageErrorExitCode;
}
GlRequirementBlock[] requirements = registry.Features.SelectMany(
value => value.Requirements)
.Concat(
registry.Extensions.SelectMany(
value => value.Requirements))
.ToArray();
Console.WriteLine("OpenGL registry");
WriteProperty("source", registry.SourcePath);
WriteProperty("types", registry.Types.Count);
WriteProperty("kinds", registry.Kinds.Count);
WriteProperty("enum groups", registry.EnumGroups.Count);
WriteProperty("enum blocks", registry.EnumBlocks.Count);
WriteProperty(
"enums",
registry.EnumBlocks.Sum(value => value.Enums.Count));
WriteProperty(
"unused enum ranges",
registry.EnumBlocks.Sum(value => value.UnusedRanges.Count));
WriteProperty("commands", registry.Commands.Count);
WriteProperty(
"parameters",
registry.Commands.Sum(value => value.Parameters.Count));
WriteProperty("features", registry.Features.Count);
WriteProperty("extensions", registry.Extensions.Count);
WriteProperty(
"require blocks",
requirements.Count(
value => value.Operation == GlRequirementOperation.Require));
WriteProperty(
"remove blocks",
requirements.Count(
value => value.Operation == GlRequirementOperation.Remove));
WriteProperty(
"named references",
requirements.Sum(value => value.References.Count));
return SuccessExitCode;
}
private static int QueryList(
GlRegistryModel registry,
IReadOnlyList<string> arguments
)
{
if (!TryReadListOptions(
arguments,
out RegistryObjectKind kind,
out string? filter,
out int limit,
out string? error))
{
return WriteUsageError(error);
}
RegistrySearchResult[] values = Enumerate(registry, kind)
.Where(
value => filter is null || value.Name.Contains(
filter,
StringComparison.OrdinalIgnoreCase))
.ToArray();
if (values.Length == 0)
{
Console.Error.WriteLine(
filter is null
? $"No {ReadKindName(kind, true)} are registered."
: $"No {ReadKindName(kind, true)} contain '{filter}'.");
return QueryErrorExitCode;
}
int displayedCount = Math.Min(values.Length, limit);
for (int index = 0; index < displayedCount; index++)
{
WriteCompactResult(index + 1, values[index]);
}
WriteTruncation(values.Length, displayedCount);
return SuccessExitCode;
}
private static int QueryFind(
GlRegistryModel registry,
IReadOnlyList<string> arguments
)
{
if (!TryReadFindOptions(
arguments,
out string text,
out bool exact,
out int limit,
out string? error))
{
return WriteUsageError(error);
}
RegistrySearchResult[] values = Enumerate(registry)
.Where(
value => exact
? string.Equals(
value.Name,
text,
StringComparison.OrdinalIgnoreCase)
: value.Name.Contains(
text,
StringComparison.OrdinalIgnoreCase))
.OrderBy(
value => value.Name,
StringComparer.Ordinal)
.ThenBy(value => value.Kind)
.ToArray();
if (values.Length == 0)
{
Console.Error.WriteLine($"No registry names match '{text}'.");
return QueryErrorExitCode;
}
int displayedCount = Math.Min(values.Length, limit);
for (int index = 0; index < displayedCount; index++)
{
WriteCompactResult(index + 1, values[index]);
}
WriteTruncation(values.Length, displayedCount);
return SuccessExitCode;
}
private static int QueryShow(
GlRegistryModel registry,
IReadOnlyList<string> arguments
)
{
if (!TryReadShowOptions(
arguments,
out RegistryObjectKind kind,
out string name,
out int limit,
out string? error))
{
return WriteUsageError(error);
}
RegistrySearchResult[] values = Enumerate(registry, kind)
.Where(
value => string.Equals(
value.Name,
name,
StringComparison.Ordinal))
.ToArray();
if (values.Length == 0)
{
Console.Error.WriteLine(
$"No {ReadKindName(kind, false)} named '{name}' is registered.");
Console.Error.WriteLine($"Try: find {name}");
return QueryErrorExitCode;
}
for (int index = 0; index < values.Length; index++)
{
if (index != 0)
{
Console.WriteLine();
}
if (values.Length > 1)
{
Console.WriteLine($"[{index + 1}]");
}
WriteDetailedResult(values[index], limit);
}
return SuccessExitCode;
}
private static IEnumerable<RegistrySearchResult> Enumerate(
GlRegistryModel registry
)
{
foreach (RegistryObjectKind kind in Enum.GetValues<RegistryObjectKind>())
{
foreach (RegistrySearchResult value in Enumerate(registry, kind))
{
yield return value;
}
}
}
private static IEnumerable<RegistrySearchResult> Enumerate(
GlRegistryModel registry,
RegistryObjectKind kind
)
{
return kind switch
{
RegistryObjectKind.Type => registry.Types.Select(
value => new RegistrySearchResult(
kind,
value.Name,
value,
value.Element)),
RegistryObjectKind.Kind => registry.Kinds.Select(
value => new RegistrySearchResult(
kind,
value.Name,
value,
value.Element)),
RegistryObjectKind.EnumGroup => registry.EnumGroups.Select(
value => new RegistrySearchResult(
kind,
value.Name,
value,
value.Element)),
RegistryObjectKind.Enum => registry.EnumBlocks.SelectMany(
value => value.Enums).Select(
value => new RegistrySearchResult(
kind,
value.Name,
value,
value.Element)),
RegistryObjectKind.Command => registry.Commands.Select(
value => new RegistrySearchResult(
kind,
value.Name,
value,
value.Element)),
RegistryObjectKind.Feature => registry.Features.Select(
value => new RegistrySearchResult(
kind,
value.Name,
value,
value.Element)),
RegistryObjectKind.Extension => registry.Extensions.Select(
value => new RegistrySearchResult(
kind,
value.Name,
value,
value.Element)),
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
}
private static void WriteCompactResult(
int index,
RegistrySearchResult result
)
{
Console.WriteLine(
$"[{index}] {ReadKindName(result.Kind, false)}: {result.Name}");
switch (result.Value)
{
case GlTypeDefinition type:
WriteProperty("declaration", type.DeclarationText);
break;
case GlKindDefinition kind:
WriteProperty("description", kind.Description);
break;
case GlEnumGroup group:
WriteProperty("members", group.Members.Count);
break;
case GlEnumDefinition enumValue:
WriteOptionalProperty("value", enumValue.Value);
WriteOptionalProperty("alias", enumValue.Alias);
WriteOptionalProperty("group", enumValue.Group);
break;
case GlCommandDefinition command:
WriteProperty("declaration", ReadCommandDeclaration(command));
break;
case GlFeatureDefinition feature:
WriteProperty("api", feature.Api);
WriteProperty("version", feature.Number);
break;
case GlExtensionDefinition extension:
WriteOptionalProperty("supported", extension.SupportedApis);
break;
}
WriteProperty("line", result.Element.Location.LineNumber);
}
private static void WriteDetailedResult(
RegistrySearchResult result,
int referenceLimit
)
{
switch (result.Value)
{
case GlTypeDefinition type:
WriteType(type);
break;
case GlKindDefinition kind:
WriteKind(kind);
break;
case GlEnumGroup group:
WriteEnumGroup(group, referenceLimit);
break;
case GlEnumDefinition enumValue:
WriteEnum(enumValue);
break;
case GlCommandDefinition command:
WriteCommand(command);
break;
case GlFeatureDefinition feature:
Console.WriteLine($"feature: {feature.Name}");
WriteProperty("api", feature.Api);
WriteProperty("version", feature.Number);
WriteOptionalProperty("protect", feature.Protect);
WriteOptionalProperty("comment", feature.Comment);
WriteProperty("line", feature.Element.Location.LineNumber);
WriteRequirements(feature.Requirements, referenceLimit);
break;
case GlExtensionDefinition extension:
Console.WriteLine($"extension: {extension.Name}");
WriteOptionalProperty("supported", extension.SupportedApis);
WriteOptionalProperty("protect", extension.Protect);
WriteOptionalProperty("comment", extension.Comment);
WriteProperty("line", extension.Element.Location.LineNumber);
WriteRequirements(extension.Requirements, referenceLimit);
break;
}
}
private static void WriteType(
GlTypeDefinition value
)
{
Console.WriteLine($"type: {value.Name}");
WriteProperty("declaration", value.DeclarationText);
WriteOptionalProperty("api", value.Api);
WriteOptionalProperty("requires", value.RequiredType);
WriteOptionalProperty("category", value.Category);
WriteOptionalProperty("comment", value.Comment);
WriteProperty("line", value.Element.Location.LineNumber);
}
private static void WriteKind(
GlKindDefinition value
)
{
Console.WriteLine($"kind: {value.Name}");
WriteProperty("description", value.Description);
WriteProperty("line", value.Element.Location.LineNumber);
}
private static void WriteEnumGroup(
GlEnumGroup value,
int limit
)
{
Console.WriteLine($"enum group: {value.Name}");
WriteOptionalProperty("comment", value.Comment);
WriteProperty("line", value.Element.Location.LineNumber);
WriteProperty("members", value.Members.Count);
int displayedCount = Math.Min(value.Members.Count, limit);
for (int index = 0; index < displayedCount; index++)
{
Console.WriteLine($" [{index + 1}] {value.Members[index].Name}");
}
WriteIndentedTruncation(value.Members.Count, displayedCount, 4);
}
private static void WriteEnum(
GlEnumDefinition value
)
{
Console.WriteLine($"enum: {value.Name}");
WriteOptionalProperty("value", value.Value);
WriteOptionalProperty("api", value.Api);
WriteOptionalProperty("type suffix", value.TypeSuffix);
WriteOptionalProperty("group", value.Group);
WriteOptionalProperty("alias", value.Alias);
WriteOptionalProperty("comment", value.Comment);
WriteProperty("line", value.Element.Location.LineNumber);
}
private static void WriteCommand(
GlCommandDefinition value
)
{
Console.WriteLine($"command: {value.Name}");
WriteProperty("declaration", ReadCommandDeclaration(value));
WriteProperty("line", value.Element.Location.LineNumber);
WriteProperty("return declaration", value.Return.DeclarationText);
WriteOptionalProperty("return type", value.Return.TypeName);
WriteOptionalProperty("return group", value.Return.Group);
WriteOptionalProperty("return kind", value.Return.Kind);
WriteOptionalProperty("return class", value.Return.ObjectClass);
WriteOptionalProperty("comment", value.Comment);
WriteProperty("parameters", value.Parameters.Count);
for (int index = 0; index < value.Parameters.Count; index++)
{
GlParameterDefinition parameter = value.Parameters[index];
Console.WriteLine($" [{index + 1}] {parameter.Name}");
WriteIndentedProperty("declaration", parameter.DeclarationText, 6);
WriteIndentedOptionalProperty("type", parameter.TypeName, 6);
WriteIndentedOptionalProperty("group", parameter.Group, 6);
WriteIndentedOptionalProperty("kind", parameter.Kind, 6);
WriteIndentedOptionalProperty("class", parameter.ObjectClass, 6);
WriteIndentedOptionalProperty("length", parameter.Length, 6);
WriteIndentedProperty("line", parameter.Element.Location.LineNumber, 6);
}
WriteNamedReferences("aliases", value.Aliases);
WriteNamedReferences("vector equivalents", value.VectorEquivalents);
if (value.Protocols.Count != 0)
{
WriteProperty("protocols", value.Protocols.Count);
foreach (GlCommandProtocol protocol in value.Protocols)
{
Console.WriteLine($" {protocol.Type}: {protocol.Opcode}");
WriteIndentedOptionalProperty("name", protocol.Name, 6);
WriteIndentedOptionalProperty("comment", protocol.Comment, 6);
}
}
}
private static void WriteRequirements(
IReadOnlyList<GlRequirementBlock> requirements,
int referenceLimit
)
{
WriteProperty("requirement blocks", requirements.Count);
for (int index = 0; index < requirements.Count; index++)
{
GlRequirementBlock block = requirements[index];
Console.WriteLine(
$" [{index + 1}] " +
$"{(block.Operation == GlRequirementOperation.Require ? "require" : "remove")}");
WriteIndentedOptionalProperty("api", block.Api, 6);
WriteIndentedOptionalProperty("profile", block.Profile, 6);
WriteIndentedOptionalProperty("comment", block.Comment, 6);
WriteIndentedProperty("references", block.References.Count, 6);
int displayedCount = Math.Min(block.References.Count, referenceLimit);
for (int referenceIndex = 0;
referenceIndex < displayedCount;
referenceIndex++)
{
GlRegistryReference reference = block.References[referenceIndex];
Console.WriteLine(
$" {ReadReferenceKind(reference.Kind)}: {reference.Name}");
}
WriteIndentedTruncation(block.References.Count, displayedCount, 8);
}
}
private static void WriteNamedReferences(
string name,
IReadOnlyList<GlNamedReference> values
)
{
if (values.Count == 0)
{
return;
}
WriteProperty(name, values.Count);
foreach (GlNamedReference value in values)
{
Console.WriteLine($" - {value.Name}");
}
}
private static string ReadCommandDeclaration(
GlCommandDefinition command
)
{
string returnType = command.Return.DeclarationText.Replace(
command.Name,
string.Empty,
StringComparison.Ordinal).Trim();
return $"{returnType} {command.Name}(" +
string.Join(", ", command.Parameters.Select(value => value.DeclarationText)) +
")";
}
private static bool TryReadListOptions(
IReadOnlyList<string> arguments,
out RegistryObjectKind kind,
out string? filter,
out int limit,
out string? error
)
{
kind = default;
if (arguments.Count == 0 || !TryReadKind(arguments[0], out kind))
{
filter = null;
limit = 0;
error = "list requires a registry category: types, kinds, groups, enums, " +
"commands, features, or extensions.";
return false;
}
filter = null;
limit = ShowAllResults;
int index = 1;
if (index < arguments.Count && !arguments[index].StartsWith(
"--",
StringComparison.Ordinal))
{
filter = arguments[index++];
}
while (index < arguments.Count)
{
string argument = arguments[index++];
if ((argument == "--showcount" || argument == "--limit") &&
TryReadPositiveInt(
arguments,
ref index,
out limit))
{
continue;
}
error = $"Unknown or invalid list option '{argument}'.";
return false;
}
error = null;
return true;
}
private static bool TryReadFindOptions(
IReadOnlyList<string> arguments,
out string text,
out bool exact,
out int limit,
out string? error
)
{
if (arguments.Count == 0 || arguments[0].StartsWith(
"--",
StringComparison.Ordinal))
{
text = string.Empty;
exact = false;
limit = 0;
error = "find requires a registry name or partial name.";
return false;
}
text = arguments[0];
exact = false;
limit = ShowAllResults;
int index = 1;
while (index < arguments.Count)
{
string argument = arguments[index++];
if (argument == "--exact")
{
exact = true;
continue;
}
if ((argument == "--showcount" || argument == "--limit") &&
TryReadPositiveInt(
arguments,
ref index,
out limit))
{
continue;
}
error = $"Unknown or invalid find option '{argument}'.";
return false;
}
error = null;
return true;
}
private static bool TryReadShowOptions(
IReadOnlyList<string> arguments,
out RegistryObjectKind kind,
out string name,
out int limit,
out string? error
)
{
kind = default;
if (arguments.Count < 2 || !TryReadKind(arguments[0], out kind))
{
name = string.Empty;
limit = 0;
error = "show requires a registry category and exact name.";
return false;
}
name = arguments[1];
limit = ShowAllResults;
int index = 2;
while (index < arguments.Count)
{
string argument = arguments[index++];
if ((argument == "--showcount" || argument == "--limit") &&
TryReadPositiveInt(
arguments,
ref index,
out limit))
{
continue;
}
error = $"Unknown or invalid show option '{argument}'.";
return false;
}
error = null;
return true;
}
private static bool TryReadKind(
string value,
out RegistryObjectKind kind
)
{
kind = value.ToUpperInvariant() switch
{
"TYPE" or "TYPES" => RegistryObjectKind.Type,
"KIND" or "KINDS" => RegistryObjectKind.Kind,
"GROUP" or "GROUPS" or "ENUMGROUP" or "ENUMGROUPS" =>
RegistryObjectKind.EnumGroup,
"ENUM" or "ENUMS" => RegistryObjectKind.Enum,
"COMMAND" or "COMMANDS" or "FUNCTION" or "FUNCTIONS" =>
RegistryObjectKind.Command,
"FEATURE" or "FEATURES" or "VERSION" or "VERSIONS" =>
RegistryObjectKind.Feature,
"EXTENSION" or "EXTENSIONS" => RegistryObjectKind.Extension,
_ => default,
};
return value.ToUpperInvariant() is
"TYPE" or "TYPES" or
"KIND" or "KINDS" or
"GROUP" or "GROUPS" or "ENUMGROUP" or "ENUMGROUPS" or
"ENUM" or "ENUMS" or
"COMMAND" or "COMMANDS" or "FUNCTION" or "FUNCTIONS" or
"FEATURE" or "FEATURES" or "VERSION" or "VERSIONS" or
"EXTENSION" or "EXTENSIONS";
}
private static string ReadKindName(
RegistryObjectKind kind,
bool plural
)
{
string name = kind switch
{
RegistryObjectKind.Type => "type",
RegistryObjectKind.Kind => "kind",
RegistryObjectKind.EnumGroup => "enum group",
RegistryObjectKind.Enum => "enum",
RegistryObjectKind.Command => "command",
RegistryObjectKind.Feature => "feature",
RegistryObjectKind.Extension => "extension",
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
return plural ? $"{name}s" : name;
}
private static string ReadReferenceKind(
GlRegistryReferenceKind kind
)
{
return kind switch
{
GlRegistryReferenceKind.Type => "type",
GlRegistryReferenceKind.Enum => "enum",
GlRegistryReferenceKind.Command => "command",
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
}
private static bool TryReadPositiveInt(
IReadOnlyList<string> arguments,
ref int index,
out int value
)
{
if (index >= arguments.Count)
{
value = 0;
return false;
}
return int.TryParse(
arguments[index++],
NumberStyles.None,
CultureInfo.InvariantCulture,
out value) && value > 0;
}
private static bool ExpectNoArguments(
string command,
IReadOnlyCollection<string> arguments
)
{
if (arguments.Count == 0)
{
return true;
}
Console.Error.WriteLine($"error: {command} does not accept arguments.");
return false;
}
private static int WriteUsageError(
string? error
)
{
Console.Error.WriteLine($"error: {error}");
return UsageErrorExitCode;
}
private static void WriteTruncation(
int totalCount,
int displayedCount
)
{
if (displayedCount != totalCount)
{
Console.WriteLine($"... {totalCount - displayedCount} more result(s)");
}
}
private static void WriteIndentedTruncation(
int totalCount,
int displayedCount,
int indentation
)
{
if (displayedCount != totalCount)
{
Console.WriteLine(
$"{new string(' ', indentation)}... " +
$"{totalCount - displayedCount} more item(s)");
}
}
private static void WriteProperty(
string name,
object value
)
{
WriteIndentedProperty(name, value, 2);
}
private static void WriteOptionalProperty(
string name,
string? value
)
{
WriteIndentedOptionalProperty(name, value, 2);
}
private static void WriteIndentedProperty(
string name,
object value,
int indentation
)
{
Console.WriteLine($"{new string(' ', indentation)}{name}: {value}");
}
private static void WriteIndentedOptionalProperty(
string name,
string? value,
int indentation
)
{
if (value is not null)
{
WriteIndentedProperty(name, value, indentation);
}
}
private sealed record RegistrySearchResult(
RegistryObjectKind Kind,
string Name,
object Value,
GlRegistryElement Element
);
private enum RegistryObjectKind
{
Type,
Kind,
EnumGroup,
Enum,
Command,
Feature,
Extension,
}
}
}
@@ -0,0 +1,59 @@
// -----------------------------------------------------------------------------
// file="TargetRegistryCli"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL
{
internal static class TargetRegistryCli
{
private const int SuccessExitCode = 0;
private const int UsageErrorExitCode = 2;
public static int Execute(
GlRegistryProjection projection,
IReadOnlyList<string> arguments
)
{
ArgumentNullException.ThrowIfNull(projection);
ArgumentNullException.ThrowIfNull(arguments);
if (arguments.Count > 1 || arguments.Count == 1 && !string.Equals(
arguments[0],
"summary",
StringComparison.OrdinalIgnoreCase))
{
Console.Error.WriteLine("error: target accepts only the 'summary' command.");
return UsageErrorExitCode;
}
Console.WriteLine("Registry target projection");
WriteProperty("api", projection.Api);
WriteProperty("profile", projection.Profile);
WriteProperty("maximum version", projection.MaximumVersion);
WriteProperty("features", projection.Features.Count);
WriteProperty("commands", projection.Commands.Count);
WriteProperty("enums", projection.Enums.Count);
WriteProperty("extensions", 0);
WriteProperty("source commands", projection.Source.Commands.Count);
WriteProperty("source extensions", projection.Source.Extensions.Count);
return SuccessExitCode;
}
private static void WriteProperty(
string name,
object value
)
{
Console.WriteLine($" {name}: {value}");
}
}
}
@@ -0,0 +1,20 @@
// -----------------------------------------------------------------------------
// file="XmlTreeDocument"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Xml.Linq;
namespace StgSharp.GenerateGL.Tree
{
/// <summary>
/// Holds the original XML text alongside its parsed tree.
/// </summary>
internal sealed record XmlTreeDocument(
string SourcePath,
string SourceText,
XDocument Tree
);
}
@@ -0,0 +1,56 @@
// -----------------------------------------------------------------------------
// file="XmlTreeReader"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.IO;
using System.Xml;
using System.Xml.Linq;
namespace StgSharp.GenerateGL.Tree
{
/// <summary>
/// Loads XML as a generic tree without interpreting OpenGL semantics.
/// </summary>
internal static class XmlTreeReader
{
public static XmlTreeDocument Load(
string path
)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
string sourcePath = Path.GetFullPath(path);
string sourceText = File.ReadAllText(sourcePath);
XmlReaderSettings settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
IgnoreComments = false,
IgnoreWhitespace = false,
XmlResolver = null,
};
XDocument tree;
using (StringReader textReader = new StringReader(sourceText))
using (XmlReader reader = XmlReader.Create(textReader, settings, sourcePath))
{
tree = XDocument.Load(
reader,
LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
}
if (tree.Root is null)
{
throw new InvalidDataException(
$"XML document '{sourcePath}' does not contain a root element.");
}
return new XmlTreeDocument(sourcePath, sourceText, tree);
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
# Khronos OpenGL Registry input
This directory intentionally does not track the third-party Khronos registry files. Before building or running `StgSharp.GenerateGL`, supply a registry snapshot from the canonical [KhronosGroup/OpenGL-Registry](https://github.com/KhronosGroup/OpenGL-Registry) repository.
1. Clone or download one coherent revision of `KhronosGroup/OpenGL-Registry`.
2. Copy the contents of its `xml/` directory directly into this directory.
3. Keep the upstream directory layout unchanged.
The generator currently requires this exact path:
```text
tools/Nitload.GenerateGL/xml/gl.xml
```
Do not introduce an additional directory level such as `xml/xml/gl.xml`. Files such as `glx.xml`, `wgl.xml`, `registry.rnc`, and the upstream registry scripts may be copied alongside `gl.xml`, but the current desktop OpenGL generator only consumes `gl.xml`.
For reproducible generation, use files from the same upstream commit and record that commit hash with the generated-code change. All copied files in this directory are ignored by Git except this README.