//----------------------------------------------------------------------- // ----------------------------------------------------------------------- // file="Generator" // Project: StgSharp // AuthorGroup: Nitload // Copyright (c) Nitload. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // ----------------------------------------------------------------------- // ----------------------------------------------------------------------- using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace GenerateGL.CodeGen { internal sealed class Generator { private static readonly string[] TargetVersions = { "3.3", "4.0", "4.1", "4.2", "4.3", "4.4", "4.5", "4.6" }; private static readonly FrozenDictionary ReservedNames = new Dictionary(StringComparer.Ordinal) { { "string", "@string" }, { "class", "@class" }, { "namespace", "@namespace" }, { "checked", "@checked" }, { "unchecked", "@unchecked" }, { "ref", "@ref" }, { "out", "@out" }, { "in", "@in" }, { "object", "@object" }, { "event", "@event" }, { "params", "@params" }, { "base", "@base" }, { "this", "@this" }, { "void", "@void" }, { "int", "@int" }, { "uint", "@uint" }, { "long", "@long" }, { "ulong", "@ulong" }, { "short", "@short" }, { "ushort", "@ushort" }, { "float", "@float" }, { "double", "@double" }, { "decimal", "@decimal" }, { "bool", "@bool" }, { "byte", "@byte" }, { "sbyte", "@sbyte" }, { "char", "@char" }, { "struct", "@struct" }, { "public", "@public" }, { "private", "@private" }, { "protected", "@protected" }, { "internal", "@internal" }, { "fixed", "@fixed" } }.ToFrozenDictionary(StringComparer.Ordinal); private readonly IReadOnlyDictionary _commands; private readonly IReadOnlyList _features; private readonly string _outputRoot; public Generator( IReadOnlyDictionary commands, IReadOnlyList features, string outputRoot ) { _commands = commands; _features = features; _outputRoot = outputRoot; } public void Run() { if (Directory.Exists(_outputRoot)) { Directory.Delete(_outputRoot, recursive:true); } Directory.CreateDirectory(_outputRoot); List all = new List(); List<(string Version, List Commands)> commandsByVersion = new(); HashSet previous = new HashSet(StringComparer.Ordinal); foreach (string v in TargetVersions) { IReadOnlyList cmdsUpTo = CollectCommandsUpToVersion(v); List versionCmds = cmdsUpTo.Where(c => !previous.Contains(c.Name)) .ToList(); foreach (GlCommand cmd in versionCmds) { previous.Add(cmd.Name); } all.AddRange(versionCmds); commandsByVersion.Add((v, versionCmds)); } IReadOnlyDictionary managedNames = ManagedNameResolver.Resolve( all.Where(ShouldGenerateWrapper)); foreach ((string version, List commands) in commandsByVersion) { WriteOpenGLFunctionFile(version, commands, managedNames); UniformOverloadEmitter.Write(_outputRoot, version, commands); } WriteMergedGlContextFile(all); WriteCommandHistoryFile(BuildCommandHistory()); } private IReadOnlyDictionary> BuildCommandHistory() { Dictionary> history = new(StringComparer.Ordinal); IEnumerable ordered = _features.Where(f => Version.TryParse(f.Version, out _)) .OrderBy(f => Version.Parse(f.Version)); foreach (GlFeature feature in ordered) { string version = feature.Version; foreach (string name in feature.RequiresCommands.Distinct(StringComparer.Ordinal)) { if (!_commands.ContainsKey(name) || ShouldSkip(name)) { continue; } if (!history.TryGetValue(name, out List? list)) { list = new List(); history[name] = list; } if (list.Count == 0 || list[^1].Action != CommandHistoryAction.Added || list[^1].Version != version) { list.Add(new CommandHistoryEntry(version, CommandHistoryAction.Added)); } } foreach (string name in feature.RemovedCommands.Distinct(StringComparer.Ordinal)) { if (!_commands.ContainsKey(name) || ShouldSkip(name)) { continue; } if (!history.TryGetValue(name, out List? list)) { list = new List(); history[name] = list; } if (list.Count == 0 || list[^1].Action != CommandHistoryAction.Removed || list[^1].Version != version) { list.Add(new CommandHistoryEntry(version, CommandHistoryAction.Removed)); } } } return history; } private IReadOnlyList CollectCommandsUpToVersion( string version ) { Version target = Version.Parse(version); IEnumerable ordered = _features.Where(f => Version.TryParse(f.Version, out Version? fv) && fv <= target) .OrderBy(f => Version.Parse(f.Version)); HashSet active = new HashSet(StringComparer.Ordinal); foreach (GlFeature feature in ordered) { foreach (string name in feature.RequiresCommands.Distinct(StringComparer.Ordinal)) { if (_commands.ContainsKey(name)) { active.Add(name); } } foreach (string name in feature.RemovedCommands.Distinct(StringComparer.Ordinal)) { active.Remove(name); } } return active.Select(name => _commands.TryGetValue(name, out GlCommand? c) ? c : null) .Where(c => c != null)! .Cast() .ToList(); } private static string DelegateSignature( string ret, IReadOnlyList paramTypes ) { if (paramTypes.Count == 0) { return $"delegate*<{ret}>"; } return $"delegate*<{string.Join(", ", paramTypes)}, {ret}>"; } private static string SafeName( string name ) { if (string.IsNullOrWhiteSpace(name)) { return "arg"; } if (ReservedNames.TryGetValue(name, out string? mapped)) { return mapped; } return name; } private static bool ShouldSkip( string name ) { // No skipping; include legacy/fixed-function APIs as well. return false; } private static string TrimGlPrefix( string name ) { if (name.StartsWith("gl", StringComparison.Ordinal) && name.Length > 2) { return name.Substring(2); } return name; } private void WriteCommandHistoryFile( IReadOnlyDictionary> history ) { StringBuilder sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine($"// Generated by StgSharp.GenerateGL on {DateTime.UtcNow.ToString("u", CultureInfo.InvariantCulture)}"); sb.AppendLine("// Command availability history"); sb.AppendLine("// "); sb.AppendLine("using System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\n"); sb.AppendLine("namespace StgSharp.Graphics.OpenGL"); sb.AppendLine("{"); sb.AppendLine(" internal enum CommandHistoryAction { Added, Removed }"); sb.AppendLine(); sb.AppendLine(" internal readonly record struct CommandHistoryEntry(string Version, CommandHistoryAction Action);"); sb.AppendLine(); sb.AppendLine(" internal static class GlCommandHistory"); sb.AppendLine(" {"); sb.AppendLine(" internal static readonly FrozenDictionary History = new Dictionary(StringComparer.Ordinal)"); sb.AppendLine(" {"); foreach (KeyValuePair> kvp in history.OrderBy(k => k.Key, StringComparer.Ordinal)) { string events = string.Join(", ", kvp.Value .OrderBy(v => Version.Parse(v.Version)) .Select(v => $"new CommandHistoryEntry(\"{v.Version}\", CommandHistoryAction.{v.Action})")); sb.AppendLine($" {{ \"{kvp.Key}\", new CommandHistoryEntry[] {{ {events} }} }},"); } sb.AppendLine(" }.ToFrozenDictionary(StringComparer.Ordinal);"); sb.AppendLine(" }"); sb.AppendLine("}"); string fileName = Path.Combine(_outputRoot, "GlCommandHistory.g.cs"); File.WriteAllText(fileName, sb.ToString(), Encoding.UTF8); } private void WriteMergedGlContextFile( IReadOnlyList commands ) { List distinct = commands.Where(c => !ShouldSkip(c.Name)) .GroupBy(c => c.Name, StringComparer.Ordinal) .Select(g => g.First()) .OrderBy(c => c.Name, StringComparer.Ordinal) .ToList(); StringBuilder sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine($"// Generated by StgSharp.GenerateGL on {DateTime.UtcNow.ToString("u", CultureInfo.InvariantCulture)}"); sb.AppendLine("// Aggregated GL3+ commands"); sb.AppendLine("// "); sb.AppendLine("using System;\nusing System.Runtime.CompilerServices;\n"); sb.AppendLine("namespace StgSharp.Graphics.OpenGL"); sb.AppendLine("{"); sb.AppendLine(" public unsafe partial struct OpenglContext"); sb.AppendLine(" {"); foreach (GlCommand cmd in distinct) { string ret = CsTypeMapper.MapDelegateReturn(cmd.ReturnType, cmd.ReturnPointerLevel); List paramTypes = new List(); foreach (GlParam p in cmd.Parameters) { string mapped = CsTypeMapper.MapDelegateParameter(p); paramTypes.Add(mapped); } string decl = DelegateSignature(ret, paramTypes); sb.AppendLine($" internal unsafe {decl} {cmd.Name};"); } sb.AppendLine(" }"); sb.AppendLine("}"); string fileName = Path.Combine(_outputRoot, "OpenglContext.g.cs"); File.WriteAllText(fileName, sb.ToString(), Encoding.UTF8); } private void WriteOpenGLFunctionFile( string version, IReadOnlyList commands, IReadOnlyDictionary managedNames ) { StringBuilder sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine($"// Generated by StgSharp.GenerateGL on {DateTime.UtcNow.ToString("u", CultureInfo.InvariantCulture)}"); sb.AppendLine($"// Version: {version}"); sb.AppendLine("// "); sb.AppendLine("using System;\nusing System.Runtime.CompilerServices;\nusing System.Runtime.InteropServices;\nusing StgSharp.Graphics.OpenGL;\n"); sb.AppendLine("namespace StgSharp.Graphics.OpenGL"); sb.AppendLine("{"); sb.AppendLine(" public unsafe partial class OpenGLFunction"); sb.AppendLine(" {"); foreach (GlCommand cmd in commands.Where(ShouldGenerateWrapper) .OrderBy(c => c!.Name, StringComparer.Ordinal)) { bool returnsHandle = CsTypeMapper.IsHandleReturn(cmd); string ret = returnsHandle ? "GlHandle" : CsTypeMapper.MapReturn(cmd.ReturnType, cmd.ReturnPointerLevel); string rawName = cmd.Name; // original glXxx string methodName = managedNames[rawName]; ManagedCommandPlan commandPlan = ManagedCommandPlan.Create(cmd); List paramList = new List(); List callArgs = new List(); List<(string Type,string Name,bool IsOut)> pinned = new(); foreach (ManagedParameterPlan parameterPlan in commandPlan.PublicParameters) { GlParam p = parameterPlan.Source; string pname = SafeName(p.Name); paramList.Add($"{parameterPlan.PublicType} {pname}"); if (parameterPlan.IsSpan) { pinned.Add((parameterPlan.ElementType!, pname, false)); } else if (parameterPlan.Kind == ManagedParameterKind.Out) { pinned.Add((parameterPlan.ElementType!, pname, true)); } } foreach (GlParam p in cmd.Parameters) { string pname = SafeName(p.Name); if (commandPlan.DerivedLengthSources.TryGetValue( p.Name, out string? sourceSpanName)) { string nativeType = CsTypeMapper.MapParameter(p); callArgs.Add($"checked(({nativeType}){SafeName(sourceSpanName)}.Length)"); continue; } ManagedParameterPlan parameterPlan = commandPlan.GetParameter(p.Name); if (parameterPlan.IsSpan || parameterPlan.Kind == ManagedParameterKind.Out) { callArgs.Add($"{pname}Ptr"); } else if (parameterPlan.PublicType == "GlHandle") { callArgs.Add($"{pname}.Value"); } else { callArgs.Add(pname); } } string signature = string.Join(", ", paramList); string call = string.Join(", ", callArgs); sb.AppendLine($" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); sb.AppendLine($" public {ret} {methodName}({signature})"); sb.AppendLine(" {"); foreach ((string _, string name, bool isOut) in pinned.Where(p => p.IsOut)) { sb.AppendLine($" {name} = default;"); } AppendSpanLengthGuards(sb, cmd, commandPlan); string prefix = ret == "void" ? string.Empty : "return "; string callExpression = $"Context.{rawName}({call})"; if (returnsHandle) { callExpression = $"GlHandle.Create({callExpression})"; } if (pinned.Count == 0) { sb.AppendLine($" {prefix}{callExpression};"); } else { string indent = " "; foreach ((string Type, string Name, bool IsOut) pin in pinned) { string pinSource = pin.IsOut ? $"&{pin.Name}" : pin.Name; sb.AppendLine($"{indent}fixed ({pin.Type}* {pin.Name}Ptr = {pinSource})"); sb.AppendLine($"{indent}{{"); indent += " "; } sb.AppendLine($"{indent}{prefix}{callExpression};"); for (int i = 0; i < pinned.Count; i++) { indent = indent.Substring(0, indent.Length - 4); sb.AppendLine($"{indent}}}"); } } sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); string fileName = Path.Combine( _outputRoot, $"OpenGLFunction.Raw.v{version.Replace('.', '_')}.g.cs"); File.WriteAllText(fileName, sb.ToString(), Encoding.UTF8); } private static void AppendSpanLengthGuards( StringBuilder sb, GlCommand command, ManagedCommandPlan plan ) { foreach ((string lengthName, string sourceName) in plan.DerivedLengthSources) { foreach (ManagedParameterPlan related in command.Parameters .Select(p => plan.GetParameter(p.Name)) .Where(p => p.IsSpan && p.Source.Length == lengthName && p.Source.Name != sourceName)) { string relatedName = SafeName(related.Source.Name); string safeSourceName = SafeName(sourceName); sb.AppendLine($" if ({relatedName}.Length != {safeSourceName}.Length) {{"); sb.AppendLine($" throw new ArgumentException(\"Span lengths controlled by {lengthName} must match.\", nameof({relatedName}));"); sb.AppendLine(" }"); } } foreach (ManagedParameterPlan parameter in command.Parameters .Select(p => plan.GetParameter(p.Name)).Where(p => p.IsSpan)) { string? expectedLength = BuildExpectedLengthExpression(parameter.Source, plan); if (expectedLength is null) { continue; } string parameterName = SafeName(parameter.Source.Name); sb.AppendLine($" if ({parameterName}.Length < {expectedLength}) {{"); sb.AppendLine($" throw new ArgumentException(\"Span is shorter than the OpenGL length expression '{parameter.Source.Length}'.\", nameof({parameterName}));"); sb.AppendLine(" }"); } } private static string? BuildExpectedLengthExpression( GlParam parameter, ManagedCommandPlan plan ) { string length = parameter.Length?.Replace(" ", string.Empty, StringComparison.Ordinal) ?? string.Empty; if (length.Length == 0 || length.StartsWith("COMPSIZE(", StringComparison.Ordinal) || plan.DerivedLengthSources.ContainsKey(length)) { return null; } if (int.TryParse(length, NumberStyles.None, CultureInfo.InvariantCulture, out int constantLength)) { return constantLength.ToString(CultureInfo.InvariantCulture); } ManagedParameterPlan? scalar = plan.PublicParameters.FirstOrDefault(p => p.Source.PointerLevel == 0 && p.Source.Name == length); if (scalar is not null) { return $"checked((int){SafeName(length)})"; } Match multiply = Regex.Match( length, "^(?[A-Za-z_][A-Za-z0-9_]*)\\*(?[0-9]+)$", RegexOptions.CultureInvariant); if (!multiply.Success) { multiply = Regex.Match( length, "^(?[0-9]+)\\*(?[A-Za-z_][A-Za-z0-9_]*)$", RegexOptions.CultureInvariant); } if (multiply.Success) { return $"checked((int){SafeName(multiply.Groups["name"].Value)} * {multiply.Groups["factor"].Value})"; } Match divide = Regex.Match( length, "^(?[A-Za-z_][A-Za-z0-9_]*)/(?[0-9]+)$", RegexOptions.CultureInvariant); return divide.Success ? $"checked((int){SafeName(divide.Groups["name"].Value)} / {divide.Groups["divisor"].Value})" : null; } private bool ShouldGenerateWrapper( GlCommand command ) { if (UniformOverloadEmitter.IsSetterCommand(command)) { return false; } return !IsRedundantVertexAttribVectorCommand(command); } private bool IsRedundantVertexAttribVectorCommand( GlCommand command ) { string name = command.Name; if (!name.StartsWith("glVertexAttrib", StringComparison.Ordinal) || !name.EndsWith('v')) { return false; } string scalarCommandName = name.Substring(0, name.Length - 1); return _commands.ContainsKey(scalarCommandName); } private enum CommandHistoryAction { Added, Removed } private readonly record struct CommandHistoryEntry( string Version, CommandHistoryAction Action ); } }