468 lines
23 KiB
C#
468 lines
23 KiB
C#
#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
|