// ----------------------------------------------------------------------------- // 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.Registry; namespace StgSharp.GenerateGL.Generation { /// /// Produces the first in-memory desktop OpenGL binding files. /// internal static class OpenGlGenerator { private const string ApiName = "gl"; private const string CoreProfile = "core"; private static readonly HashSet _csharpKeywords = new HashSet( 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( GlRegistryModel registry ) { ArgumentNullException.ThrowIfNull(registry); IReadOnlyList commandGroups = ReadCoreCommandGroups(registry); IReadOnlyList commands = Array.AsReadOnly( commandGroups.SelectMany(value => value.Commands).ToArray()); GlEquivalentTypeMapper typeMapper = new GlEquivalentTypeMapper(registry); ManagedApiPlanner apiPlanner = new ManagedApiPlanner(typeMapper); List managedGroups = commandGroups.Select( group => new ManagedCommandGroup( group, apiPlanner.Plan(group.Commands))).ToList(); ManagedApiPlanner.Validate( managedGroups.SelectMany(value => value.Methods)); List files = new List { new GeneratedFile("glconst.cs", GenerateConstants(registry)), new GeneratedFile( "OpenglContext.g.cs", GenerateContext(commands, typeMapper)), }; 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.Description, index == 0))); } return new GeneratedFileSet(files); } private static ReadOnlyCollection ReadCoreCommandGroups( GlRegistryModel registry ) { Dictionary definitions = new Dictionary( StringComparer.Ordinal); foreach (GlCommandDefinition command in registry.Commands) { if (!definitions.TryAdd(command.Name, command)) { throw new InvalidDataException( $"Command '{command.Name}' has more than one definition."); } } List order = new List(); HashSet known = new HashSet(StringComparer.Ordinal); HashSet active = new HashSet(StringComparer.Ordinal); Dictionary introducedVersions = new Dictionary( StringComparer.Ordinal); foreach (GlFeatureDefinition feature in registry.Features.Where( value => string.Equals(value.Api, ApiName, StringComparison.Ordinal))) { foreach (GlRequirementBlock block in feature.Requirements.Where(AppliesToCoreGl)) { foreach (GlRegistryReference reference in block.References.Where( value => value.Kind == GlRegistryReferenceKind.Command)) { if (!definitions.ContainsKey(reference.Name)) { throw new InvalidDataException( $"Feature '{feature.Name}' references undefined command " + $"'{reference.Name}'."); } if (block.Operation == GlRequirementOperation.Require) { active.Add(reference.Name); if (known.Add(reference.Name)) { order.Add(reference.Name); introducedVersions.Add(reference.Name, feature.Number); } } else { active.Remove(reference.Name); } } } } List foundation = new List(); Dictionary> versionGroups = new Dictionary>( StringComparer.Ordinal); List versionOrder = new List(); foreach (string commandName in order.Where(active.Contains)) { string version = introducedVersions[commandName]; if (IsFoundationVersion(version)) { foundation.Add(definitions[commandName]); continue; } if (!versionGroups.TryGetValue( version, out List? commands)) { commands = new List(); versionGroups.Add(version, commands); versionOrder.Add(version); } commands.Add(definitions[commandName]); } List groups = new List { 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 bool AppliesToCoreGl( 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 string GenerateConstants( GlRegistryModel registry ) { StringBuilder builder = new StringBuilder(512 * 1024); AppendGeneratedHeader(builder, "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 emitted = new Dictionary( StringComparer.Ordinal); foreach (GlEnumDefinition value in registry.EnumBlocks.SelectMany( block => block.Enums).Where(IsDesktopEnum)) { 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 bool IsDesktopEnum( GlEnumDefinition value ) { return value.Api is null || string.Equals( value.Api, ApiName, StringComparison.Ordinal); } 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 commands, GlEquivalentTypeMapper typeMapper ) { StringBuilder builder = new StringBuilder(128 * 1024); AppendGeneratedHeader(builder, "Desktop OpenGL core function pointer table"); builder.Append("namespace StgSharp.Graphics.OpenGL\n"); builder.Append("{\n"); builder.Append(" public unsafe partial struct OpenglContext\n"); builder.Append(" {\n"); foreach (GlCommandDefinition command in 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 GenerateFunctions( IReadOnlyList methods, string description, bool includeContextState ) { StringBuilder builder = new StringBuilder(512 * 1024); AppendGeneratedHeader(builder, description); builder.Append("using StgSharp.Mathematics.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) { 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 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; default: throw new InvalidOperationException( $"Unsupported managed invocation '{method.Invocation.GetType().Name}'."); } } private static void AppendDirectInvocation( StringBuilder builder, ManagedMethodDefinition method, DirectInvocation invocation ) { List fixedParameters = new List(); 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.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 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 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.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 description ) { builder.Append("// \n"); builder.Append("// Generated by StgSharp.GenerateGL.\n"); builder.Append("// "); builder.Append(description); builder.Append(".\n"); builder.Append("// \n\n"); } private sealed record ConstantExpression( string Type, string Literal ); private sealed record GlCommandGroup( string FileName, string Description, IReadOnlyList Commands ); private sealed record ManagedCommandGroup( GlCommandGroup Source, IReadOnlyList Methods ); } }