2023-03-28 13:24:16 -04:00
#if UNITY_EDITOR
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
2023-05-07 18:43:11 -04:00
using System.Runtime.InteropServices ;
2023-03-28 13:24:16 -04:00
using System.Text ;
2023-05-07 18:43:11 -04:00
using Unity.Collections.LowLevel.Unsafe ;
2023-03-28 13:24:16 -04:00
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}"/>
2023-05-07 18:43:11 -04:00
public static unsafe string GenerateCodeForDeviceLayout ( string layoutName , string defines = null , string namePrefix = "Fast" , string visibility = "public" , string @namespace = null )
2023-03-28 13:24:16 -04:00
{
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 ( @namespace ! = "" )
writer . WriteLine ( "namespace " + @namespace ) ;
writer . BeginBlock ( ) ;
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 ( ) ;
2023-05-07 18:43:11 -04:00
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 ( ) ;
2023-03-28 13:24:16 -04:00
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 ( ) ;
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