123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- #if UNITY_EDITOR
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Runtime.InteropServices;
- using System.Text;
- using Unity.Collections.LowLevel.Unsafe;
- using UnityEngine.InputSystem.Controls;
- using UnityEngine.InputSystem.Layouts;
- using UnityEngine.InputSystem.Processors;
- using UnityEngine.InputSystem.Utilities;
-
- namespace UnityEngine.InputSystem.Editor
- {
- internal static class InputLayoutCodeGenerator
- {
- public static string GenerateCodeFileForDeviceLayout(string layoutName, string fileName, string prefix = "Fast")
- {
- string defines = null;
- string @namespace = null;
- var visibility = "public";
-
- // If the file already exists, read out the changes we preserve.
- if (File.Exists(fileName))
- {
- var lines = File.ReadLines(fileName).Take(50).ToList();
-
- // Read out #defines.
- for (var i = 0; i < (lines.Count - 1); ++i)
- {
- var line = lines[i].Trim();
- if (line.StartsWith("#if "))
- defines = line.Substring("#if ".Length);
- else if (line.StartsWith("namespace "))
- @namespace = line.Substring("namespace ".Length);
- }
-
- if (lines.Any(x => x.Contains("internal partial class " + prefix)))
- visibility = "internal";
- }
-
- return GenerateCodeForDeviceLayout(layoutName,
- defines: defines, visibility: visibility, @namespace: @namespace, namePrefix: prefix);
- }
-
- /// <summary>
- /// Generate C# code that for the given device layout called <paramref name="layoutName"/> instantly creates
- /// an <see cref="InputDevice"/> equivalent to what the input system would create by manually interpreting
- /// the given <see cref="InputControlLayout"/>.
- /// </summary>
- /// <param name="layoutName">Name of the device layout to generate code for.</param>
- /// <param name="defines">Null/empty or a valid expression for an #if conditional compilation statement.</param>
- /// <param name="namePrefix">Prefix to prepend to the type name of <paramref name="layoutName"/>.</param>
- /// <param name="visibility">C# access modifier to use with the generated class.</param>
- /// <param name="namespace">Namespace to put the generated class in. If <c>null</c>, namespace of type behind <paramref name="layoutName"/> will be used.</param>
- /// <returns>C# source code for a precompiled version of the device layout.</returns>
- /// <remarks>
- /// The code generated by this method will be many times faster than the reflection-based <see cref="InputDevice"/>
- /// creation normally performed by the input system. It will also create less GC heap garbage.
- ///
- /// The downside to the generated code is that the makeup of the device is hardcoded and can no longer
- /// be changed by altering the <see cref="InputControlLayout"/> setup of the system.
- ///
- /// Note that it is possible to use this method with layouts generated on-the-fly by layout builders such as
- /// the one employed for <see cref="HID"/>. However, this must be done at compile/build time and can thus not
- /// be done for devices dynamically discovered at runtime. When this is acceptable, it is a way to dramatically
- /// speed up the creation of these devices.
- /// </remarks>
- /// <seealso cref="InputSystem.RegisterPrecompiledLayout{T}"/>
- public static unsafe string GenerateCodeForDeviceLayout(string layoutName, string defines = null, string namePrefix = "Fast", string visibility = "public", string @namespace = null)
- {
- if (string.IsNullOrEmpty(layoutName))
- throw new ArgumentNullException(nameof(layoutName));
-
- // Produce a device from the layout.
- var device = InputDevice.Build<InputDevice>(layoutName, noPrecompiledLayouts: true);
-
- // Get info about base type.
- var baseType = device.GetType();
- var baseTypeName = baseType.Name;
- var baseTypeNamespace = baseType.Namespace;
-
- // Begin generating code.
- var writer = new InputActionCodeGenerator.Writer
- {
- buffer = new StringBuilder()
- };
-
- writer.WriteLine(CSharpCodeHelpers.MakeAutoGeneratedCodeHeader("com.unity.inputsystem:InputLayoutCodeGenerator",
- InputSystem.version.ToString(),
- $"\"{layoutName}\" layout"));
-
- // Defines.
- if (defines != null)
- {
- writer.WriteLine($"#if {defines}");
- writer.WriteLine();
- }
-
- if (@namespace == null)
- @namespace = baseTypeNamespace;
-
- writer.WriteLine("using UnityEngine.InputSystem;");
- writer.WriteLine("using UnityEngine.InputSystem.LowLevel;");
- writer.WriteLine("using UnityEngine.InputSystem.Utilities;");
- writer.WriteLine("");
- writer.WriteLine("// Suppress warnings from local variables for control references");
- writer.WriteLine("// that we don't end up using.");
- writer.WriteLine("#pragma warning disable CS0219");
- writer.WriteLine("");
- if (!string.IsNullOrEmpty(@namespace))
- {
- writer.WriteLine("namespace " + @namespace);
- writer.BeginBlock();
- }
-
- if (string.IsNullOrEmpty(baseTypeNamespace))
- writer.WriteLine($"{visibility} partial class {namePrefix}{baseTypeName} : {baseTypeName}");
- else
- writer.WriteLine($"{visibility} partial class {namePrefix}{baseTypeName} : {baseTypeNamespace}.{baseTypeName}");
-
- writer.BeginBlock();
-
- // "Metadata". ATM this is simply a flat, semicolon-separated list of names for layouts and processors that
- // we depend on. If any of them are touched, the precompiled layout should be considered invalidated.
- var internedLayoutName = new InternedString(layoutName);
- var allControls = device.allControls;
- var usedControlLayouts = allControls.Select(x => x.m_Layout).Distinct().ToList();
- var layoutDependencies = string.Join(";",
- usedControlLayouts.SelectMany(l => InputControlLayout.s_Layouts.GetBaseLayouts(l))
- .Union(InputControlLayout.s_Layouts.GetBaseLayouts(internedLayoutName)));
- var processorDependencies = string.Join(";",
- allControls.SelectMany(c => c.GetProcessors()).Select(p => InputProcessor.s_Processors.FindNameForType(p.GetType()))
- .Where(n => !n.IsEmpty()).Distinct());
- var metadata = string.Join(";", processorDependencies, layoutDependencies);
- writer.WriteLine($"public const string metadata = \"{metadata}\";");
-
- // Constructor.
- writer.WriteLine($"public {namePrefix}{baseTypeName}()");
- writer.BeginBlock();
-
- var usagesForEachControl = device.m_UsagesForEachControl;
- var usageToControl = device.m_UsageToControl;
- var aliasesForEachControl = device.m_AliasesForEachControl;
- var controlCount = allControls.Count;
- var usageCount = usagesForEachControl?.Length ?? 0;
- var aliasCount = aliasesForEachControl?.Length ?? 0;
-
- // Set up device control info.
- writer.WriteLine($"var builder = this.Setup({controlCount}, {usageCount}, {aliasCount})");
- writer.WriteLine($" .WithName(\"{device.name}\")");
- writer.WriteLine($" .WithDisplayName(\"{device.displayName}\")");
- writer.WriteLine($" .WithChildren({device.m_ChildStartIndex}, {device.m_ChildCount})");
- writer.WriteLine($" .WithLayout(new InternedString(\"{device.layout}\"))");
- writer.WriteLine($" .WithStateBlock(new InputStateBlock {{ format = new FourCC({(int)device.stateBlock.format}), sizeInBits = {device.stateBlock.sizeInBits} }});");
-
- if (device.noisy)
- writer.WriteLine("builder.IsNoisy(true);");
-
- // Add controls to device.
- writer.WriteLine();
- foreach (var layout in usedControlLayouts)
- writer.WriteLine($"var k{layout}Layout = new InternedString(\"{layout}\");");
-
- for (var i = 0; i < controlCount; ++i)
- {
- var control = allControls[i];
- var controlVariableName = MakeControlVariableName(control);
-
- writer.WriteLine("");
- writer.WriteLine($"// {control.path}");
- var parentName = "this";
- if (control.parent != device)
- parentName = MakeControlVariableName(control.parent);
- writer.WriteLine($"var {controlVariableName} = {NameOfControlMethod(controlVariableName)}(k{control.layout}Layout, {parentName});");
- }
-
- // Initialize usages array.
- if (usageCount > 0)
- {
- writer.WriteLine();
- writer.WriteLine("// Usages.");
- for (var i = 0; i < usageCount; ++i)
- writer.WriteLine(
- $"builder.WithControlUsage({i}, new InternedString(\"{usagesForEachControl[i]}\"), {MakeControlVariableName(usageToControl[i])});");
- }
-
- // Initialize aliases array.
- if (aliasCount > 0)
- {
- writer.WriteLine();
- writer.WriteLine("// Aliases.");
- for (var i = 0; i < aliasCount; ++i)
- writer.WriteLine($"builder.WithControlAlias({i}, new InternedString(\"{aliasesForEachControl[i]}\"));");
- }
-
- // Emit initializers for control getters and control arrays. This is usually what's getting set up
- // in FinishSetup(). We hardcode the look results here.
- var controlGetterProperties = new Dictionary<Type, List<PropertyInfo>>();
- var controlArrayProperties = new Dictionary<Type, List<PropertyInfo>>();
-
- writer.WriteLine();
- writer.WriteLine("// Control getters/arrays.");
- writer.EmitControlArrayInitializers(device, "this", controlArrayProperties);
- writer.EmitControlGetterInitializers(device, "this", controlGetterProperties);
-
- for (var i = 0; i < controlCount; ++i)
- {
- var control = allControls[i];
- var controlVariableName = MakeControlVariableName(control);
-
- writer.EmitControlArrayInitializers(control, controlVariableName, controlArrayProperties);
- writer.EmitControlGetterInitializers(control, controlVariableName, controlGetterProperties);
- }
-
- // State offset to control index map.
- if (device.m_StateOffsetToControlMap != null)
- {
- writer.WriteLine();
- writer.WriteLine("// State offset to control index map.");
- writer.WriteLine("builder.WithStateOffsetToControlIndexMap(new uint[]");
- writer.WriteLine("{");
- ++writer.indentLevel;
- var map = device.m_StateOffsetToControlMap;
- var entryCount = map.Length;
- for (var index = 0; index < entryCount;)
- {
- if (index != 0)
- writer.WriteLine();
- // 10 entries a line.
- writer.WriteIndent();
- for (var i = 0; i < 10 && index < entryCount; ++index, ++i)
- writer.Write((index != 0 ? ", " : "") + map[index] + "u");
- }
- writer.WriteLine();
- --writer.indentLevel;
- writer.WriteLine("});");
- }
-
- writer.WriteLine();
-
- if (device.m_ControlTreeNodes != null)
- {
- if (device.m_ControlTreeIndices == null)
- throw new InvalidOperationException(
- $"Control tree indicies was null. Ensure the '{device.displayName}' device was created without errors.");
-
- writer.WriteLine("builder.WithControlTree(new byte[]");
- writer.WriteLine("{");
- ++writer.indentLevel;
- writer.WriteLine("// Control tree nodes as bytes");
- var nodePtr = (byte*)UnsafeUtility.AddressOf(ref device.m_ControlTreeNodes[0]);
- var byteCount = device.m_ControlTreeNodes.Length * UnsafeUtility.SizeOf<InputDevice.ControlBitRangeNode>();
-
- for (var i = 0; i < byteCount;)
- {
- if (i != 0)
- writer.WriteLine();
-
- writer.WriteIndent();
- for (var j = 0; j < 30 && i < byteCount; j++, i++)
- {
- writer.Write((i != 0 ? ", " : "") + *(nodePtr + i));
- }
- }
- writer.WriteLine();
- --writer.indentLevel;
-
- writer.WriteLine("}, new ushort[]");
-
- ++writer.indentLevel;
- writer.WriteLine("{");
- ++writer.indentLevel;
- writer.WriteLine("// Control tree node indicies");
- writer.WriteLine();
-
- for (var i = 0; i < device.m_ControlTreeIndices.Length;)
- {
- if (i != 0)
- writer.WriteLine();
-
- writer.WriteIndent();
- for (var j = 0; j < 30 && i < device.m_ControlTreeIndices.Length; j++, i++)
- {
- writer.Write((i != 0 ? ", " : "") + device.m_ControlTreeIndices[i]);
- }
- }
-
- writer.WriteLine();
- --writer.indentLevel;
- writer.WriteLine("});");
- --writer.indentLevel;
- }
-
- writer.WriteLine();
-
- writer.WriteLine("builder.Finish();");
- writer.EndBlock();
-
- for (var i = 0; i < controlCount; ++i)
- {
- var control = allControls[i];
- var controlType = control.GetType();
- var controlVariableName = MakeControlVariableName(control);
- var controlFieldInits = control.GetInitializersForPublicPrimitiveTypeFields();
- writer.WriteLine();
- EmitControlMethod(writer, controlVariableName, controlType, controlFieldInits, i, control);
- }
-
- writer.EndBlock();
-
- if (!string.IsNullOrEmpty(@namespace))
- writer.EndBlock();
-
- if (defines != null)
- writer.WriteLine($"#endif // {defines}");
-
- return writer.buffer.ToString();
- }
-
- private static string NameOfControlMethod(string controlVariableName)
- {
- return $"Initialize_{controlVariableName}";
- }
-
- // We emit this as a separate method instead of directly inline to avoid generating a single massive constructor method
- // as these can lead to large build times with il2cpp and C++ compilers (https://fogbugz.unity3d.com/f/cases/1282090/).
- private static void EmitControlMethod(InputActionCodeGenerator.Writer writer, string controlVariableName, Type controlType,
- string controlFieldInits, int i, InputControl control)
- {
- var controlTypeName = controlType.FullName.Replace('+', '.');
- writer.WriteLine($"private {controlTypeName} {NameOfControlMethod(controlVariableName)}(InternedString k{control.layout}Layout, InputControl parent)");
- writer.BeginBlock();
- writer.WriteLine($"var {controlVariableName} = new {controlTypeName}{controlFieldInits};");
- writer.WriteLine($"{controlVariableName}.Setup()");
- writer.WriteLine($" .At(this, {i})");
- writer.WriteLine(" .WithParent(parent)");
- if (control.children.Count > 0)
- writer.WriteLine($" .WithChildren({control.m_ChildStartIndex}, {control.m_ChildCount})");
- writer.WriteLine($" .WithName(\"{control.name}\")");
- writer.WriteLine($" .WithDisplayName(\"{control.m_DisplayNameFromLayout.Replace("\\", "\\\\")}\")");
- if (!string.IsNullOrEmpty(control.m_ShortDisplayNameFromLayout))
- writer.WriteLine(
- $" .WithShortDisplayName(\"{control.m_ShortDisplayNameFromLayout.Replace("\\", "\\\\")}\")");
- writer.WriteLine($" .WithLayout(k{control.layout}Layout)");
- if (control.usages.Count > 0)
- writer.WriteLine($" .WithUsages({control.m_UsageStartIndex}, {control.m_UsageCount})");
- if (control.aliases.Count > 0)
- writer.WriteLine($" .WithAliases({control.m_AliasStartIndex}, {control.m_AliasCount})");
- if (control.noisy)
- writer.WriteLine(" .IsNoisy(true)");
- if (control.synthetic)
- writer.WriteLine(" .IsSynthetic(true)");
- if (control.dontReset)
- writer.WriteLine(" .DontReset(true)");
- if (control is ButtonControl)
- writer.WriteLine(" .IsButton(true)");
- writer.WriteLine(" .WithStateBlock(new InputStateBlock");
- writer.WriteLine(" {");
- writer.WriteLine($" format = new FourCC({(int) control.stateBlock.format}),");
- writer.WriteLine($" byteOffset = {control.stateBlock.byteOffset},");
- writer.WriteLine($" bitOffset = {control.stateBlock.bitOffset},");
- writer.WriteLine($" sizeInBits = {control.stateBlock.sizeInBits}");
- writer.WriteLine(" })");
- if (control.hasDefaultState)
- writer.WriteLine($" .WithDefaultState({control.m_DefaultState})");
- if (control.m_MinValue != default || control.m_MaxValue != default)
- writer.WriteLine($" .WithMinAndMax({control.m_MinValue}, {control.m_MaxValue})");
- foreach (var processor in control.GetProcessors())
- {
- var isEditorWindowSpaceProcessor = processor is EditorWindowSpaceProcessor;
- if (isEditorWindowSpaceProcessor)
- writer.WriteLine(" #if UNITY_EDITOR");
-
- var processorType = processor.GetType().FullName.Replace("+", ".");
- var valueType = InputProcessor.GetValueTypeFromType(processor.GetType());
- var fieldInits = processor.GetInitializersForPublicPrimitiveTypeFields();
-
- writer.WriteLine(
- $" .WithProcessor<InputProcessor<{valueType}>, {valueType}>(new {processorType}{fieldInits})");
-
- if (isEditorWindowSpaceProcessor)
- writer.WriteLine(" #endif");
- }
-
- writer.WriteLine(" .Finish();");
-
- if (control is KeyControl key)
- writer.WriteLine($"{controlVariableName}.keyCode = UnityEngine.InputSystem.Key.{key.keyCode};");
- else if (control is DpadControl.DpadAxisControl dpadAxis)
- writer.WriteLine($"{controlVariableName}.component = {dpadAxis.component};");
-
- writer.WriteLine($"return {controlVariableName};");
- writer.EndBlock();
- }
-
- private static string MakeControlVariableName(InputControl control)
- {
- return "ctrl" + CSharpCodeHelpers.MakeIdentifier(control.path);
- }
-
- private static void EmitControlGetterInitializers(this InputActionCodeGenerator.Writer writer, InputControl control,
- string controlVariableName, Dictionary<Type, List<PropertyInfo>> controlGetterPropertyTable)
- {
- var type = control.GetType();
- if (!controlGetterPropertyTable.TryGetValue(type, out var controlGetterProperties))
- {
- controlGetterProperties = GetControlGetterProperties(type);
- controlGetterPropertyTable[type] = controlGetterProperties;
- }
-
- foreach (var property in controlGetterProperties)
- {
- var value = (InputControl)property.GetValue(control);
- if (value == null)
- continue;
- writer.WriteLine($"{controlVariableName}.{property.Name} = {MakeControlVariableName(value)};");
- }
- }
-
- private static void EmitControlArrayInitializers(this InputActionCodeGenerator.Writer writer, InputControl control,
- string controlVariableName, Dictionary<Type, List<PropertyInfo>> controlArrayPropertyTable)
- {
- var type = control.GetType();
- if (!controlArrayPropertyTable.TryGetValue(type, out var controlArrayProperties))
- {
- controlArrayProperties = GetControlArrayProperties(type);
- controlArrayPropertyTable[type] = controlArrayProperties;
- }
-
- foreach (var property in controlArrayProperties)
- {
- var array = (Array)property.GetValue(control);
- if (array == null)
- continue;
- var arrayLength = array.Length;
- var arrayElementType = array.GetType().GetElementType();
- writer.WriteLine($"{controlVariableName}.{property.Name} = new {arrayElementType.FullName.Replace('+','.')}[{arrayLength}];");
-
- for (var i = 0; i < arrayLength; ++i)
- {
- var value = (InputControl)array.GetValue(i);
- if (value == null)
- continue;
- writer.WriteLine($"{controlVariableName}.{property.Name}[{i}] = {MakeControlVariableName(value)};");
- }
- }
- }
-
- private static List<PropertyInfo> GetControlGetterProperties(Type type)
- {
- return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
- .Where(x => typeof(InputControl).IsAssignableFrom(x.PropertyType) && x.CanRead && x.CanWrite &&
- x.GetIndexParameters().LengthSafe() == 0 && x.Name != "device" && x.Name != "parent").ToList();
- }
-
- private static List<PropertyInfo> GetControlArrayProperties(Type type)
- {
- return type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
- .Where(x => x.PropertyType.IsArray && typeof(InputControl).IsAssignableFrom(x.PropertyType.GetElementType()) && x.CanRead && x.CanWrite &&
- x.GetIndexParameters().LengthSafe() == 0).ToList();
- }
- }
- }
- #endif // UNITY_EDITOR
|