2023-03-28 13:24:16 -04:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Linq ;
using Unity.Collections ;
using UnityEngine.InputSystem.Composites ;
using UnityEngine.InputSystem.Controls ;
using Unity.Collections.LowLevel.Unsafe ;
using UnityEngine.Profiling ;
using UnityEngine.InputSystem.LowLevel ;
using UnityEngine.InputSystem.Processors ;
using UnityEngine.InputSystem.Interactions ;
using UnityEngine.InputSystem.Utilities ;
using UnityEngine.InputSystem.Layouts ;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor ;
#endif
////TODO: make diagnostics available in dev players and give it a public API to enable them
////TODO: work towards InputManager having no direct knowledge of actions
////TODO: allow pushing events into the system any which way; decouple from the buffer in NativeInputSystem being the only source
////REVIEW: change the event properties over to using IObservable?
////REVIEW: instead of RegisterInteraction and RegisterProcessor, have a generic RegisterInterface (or something)?
////REVIEW: can we do away with the 'previous == previous frame' and simply buffer flip on every value write?
////REVIEW: should we force keeping mouse/pen/keyboard/touch around in editor even if not in list of supported devices?
////REVIEW: do we want to filter out state events that result in no state change?
#pragma warning disable CS0649
namespace UnityEngine.InputSystem
{
using DeviceChangeListener = Action < InputDevice , InputDeviceChange > ;
using DeviceStateChangeListener = Action < InputDevice , InputEventPtr > ;
using LayoutChangeListener = Action < string , InputControlLayoutChange > ;
using EventListener = Action < InputEventPtr , InputDevice > ;
using UpdateListener = Action ;
/// <summary>
/// Hub of the input system.
/// </summary>
/// <remarks>
/// Not exposed. Use <see cref="InputSystem"/> as the public entry point to the system.
///
/// Manages devices, layouts, and event processing.
/// </remarks>
internal partial class InputManager
{
public ReadOnlyArray < InputDevice > devices = > new ReadOnlyArray < InputDevice > ( m_Devices , 0 , m_DevicesCount ) ;
public TypeTable processors = > m_Processors ;
public TypeTable interactions = > m_Interactions ;
public TypeTable composites = > m_Composites ;
public InputMetrics metrics
{
get
{
var result = m_Metrics ;
result . currentNumDevices = m_DevicesCount ;
result . currentStateSizeInBytes = ( int ) m_StateBuffers . totalSize ;
// Count controls.
result . currentControlCount = m_DevicesCount ;
for ( var i = 0 ; i < m_DevicesCount ; + + i )
result . currentControlCount + = m_Devices [ i ] . allControls . Count ;
// Count layouts.
result . currentLayoutCount = m_Layouts . layoutTypes . Count ;
result . currentLayoutCount + = m_Layouts . layoutStrings . Count ;
result . currentLayoutCount + = m_Layouts . layoutBuilders . Count ;
result . currentLayoutCount + = m_Layouts . layoutOverrides . Count ;
return result ;
}
}
public InputSettings settings
{
get
{
Debug . Assert ( m_Settings ! = null ) ;
return m_Settings ;
}
set
{
if ( value = = null )
throw new ArgumentNullException ( nameof ( value ) ) ;
if ( m_Settings = = value )
return ;
m_Settings = value ;
ApplySettings ( ) ;
}
}
public InputUpdateType updateMask
{
get = > m_UpdateMask ;
set
{
// In editor, we don't allow disabling editor updates.
#if UNITY_EDITOR
value | = InputUpdateType . Editor ;
#endif
if ( m_UpdateMask = = value )
return ;
m_UpdateMask = value ;
// Recreate state buffers.
if ( m_DevicesCount > 0 )
ReallocateStateBuffers ( ) ;
}
}
public InputUpdateType defaultUpdateType
{
get
{
if ( m_CurrentUpdate ! = default )
return m_CurrentUpdate ;
#if UNITY_EDITOR
if ( ! m_RunPlayerUpdatesInEditMode & & ( ! gameIsPlaying | | ! gameHasFocus ) )
return InputUpdateType . Editor ;
#endif
return m_UpdateMask . GetUpdateTypeForPlayer ( ) ;
}
}
public float pollingFrequency
{
get = > m_PollingFrequency ;
set
{
////REVIEW: allow setting to zero to turn off polling altogether?
if ( value < = 0 )
throw new ArgumentException ( "Polling frequency must be greater than zero" , "value" ) ;
m_PollingFrequency = value ;
if ( m_Runtime ! = null )
m_Runtime . pollingFrequency = value ;
}
}
public event DeviceChangeListener onDeviceChange
{
add = > m_DeviceChangeListeners . AddCallback ( value ) ;
remove = > m_DeviceChangeListeners . RemoveCallback ( value ) ;
}
public event DeviceStateChangeListener onDeviceStateChange
{
add = > m_DeviceStateChangeListeners . AddCallback ( value ) ;
remove = > m_DeviceStateChangeListeners . RemoveCallback ( value ) ;
}
public event InputDeviceCommandDelegate onDeviceCommand
{
add = > m_DeviceCommandCallbacks . AddCallback ( value ) ;
remove = > m_DeviceCommandCallbacks . RemoveCallback ( value ) ;
}
////REVIEW: would be great to have a way to sort out precedence between two callbacks
public event InputDeviceFindControlLayoutDelegate onFindControlLayoutForDevice
{
add
{
m_DeviceFindLayoutCallbacks . AddCallback ( value ) ;
// Having a new callback on this event can change the set of devices we recognize.
// See if there's anything in the list of available devices that we can now turn
// into an InputDevice whereas we couldn't before.
//
// NOTE: A callback could also impact already existing devices and theoretically alter
// what layout we would have used for those. We do *NOT* retroactively apply
// those changes.
AddAvailableDevicesThatAreNowRecognized ( ) ;
}
remove = > m_DeviceFindLayoutCallbacks . RemoveCallback ( value ) ;
}
public event LayoutChangeListener onLayoutChange
{
add = > m_LayoutChangeListeners . AddCallback ( value ) ;
remove = > m_LayoutChangeListeners . RemoveCallback ( value ) ;
}
////TODO: add InputEventBuffer struct that uses NativeArray underneath
////TODO: make InputEventTrace use NativeArray
////TODO: introduce an alternative that consumes events in bulk
public event EventListener onEvent
{
add = > m_EventListeners . AddCallback ( value ) ;
remove = > m_EventListeners . RemoveCallback ( value ) ;
}
public event UpdateListener onBeforeUpdate
{
add
{
InstallBeforeUpdateHookIfNecessary ( ) ;
m_BeforeUpdateListeners . AddCallback ( value ) ;
}
remove = > m_BeforeUpdateListeners . RemoveCallback ( value ) ;
}
public event UpdateListener onAfterUpdate
{
add = > m_AfterUpdateListeners . AddCallback ( value ) ;
remove = > m_AfterUpdateListeners . RemoveCallback ( value ) ;
}
public event Action onSettingsChange
{
add = > m_SettingsChangedListeners . AddCallback ( value ) ;
remove = > m_SettingsChangedListeners . RemoveCallback ( value ) ;
}
public bool isProcessingEvents = > m_InputEventStream . isOpen ;
#if UNITY_EDITOR
private bool m_RunPlayerUpdatesInEditMode ;
/// <summary>
/// If true, consider the editor to be in "perpetual play mode". Meaning, we ignore editor
/// updates and just go and continuously process Dynamic/Fixed/BeforeRender regardless of
/// whether we're in play mode or not.
///
/// In this mode, we also ignore game view focus.
/// </summary>
public bool runPlayerUpdatesInEditMode
{
get = > m_RunPlayerUpdatesInEditMode ;
set = > m_RunPlayerUpdatesInEditMode = value ;
}
#endif
private bool gameIsPlaying = >
#if UNITY_EDITOR
( m_Runtime . isInPlayMode & & ! m_Runtime . isPaused ) | | m_RunPlayerUpdatesInEditMode ;
#else
true ;
#endif
private bool gameHasFocus = >
#if UNITY_EDITOR
m_RunPlayerUpdatesInEditMode | | m_HasFocus | | gameShouldGetInputRegardlessOfFocus ;
#else
m_HasFocus | | gameShouldGetInputRegardlessOfFocus ;
#endif
private bool gameShouldGetInputRegardlessOfFocus = >
m_Settings . backgroundBehavior = = InputSettings . BackgroundBehavior . IgnoreFocus
#if UNITY_EDITOR
& & m_Settings . editorInputBehaviorInPlayMode = = InputSettings . EditorInputBehaviorInPlayMode . AllDeviceInputAlwaysGoesToGameView
#endif
;
////TODO: when registering a layout that exists as a layout of a different type (type vs string vs constructor),
//// remove the existing registration
// Add a layout constructed from a type.
// If a layout with the same name already exists, the new layout
// takes its place.
public void RegisterControlLayout ( string name , Type type )
{
if ( string . IsNullOrEmpty ( name ) )
throw new ArgumentNullException ( nameof ( name ) ) ;
if ( type = = null )
throw new ArgumentNullException ( nameof ( type ) ) ;
// Note that since InputDevice derives from InputControl, isDeviceLayout implies
// isControlLayout to be true as well.
var isDeviceLayout = typeof ( InputDevice ) . IsAssignableFrom ( type ) ;
var isControlLayout = typeof ( InputControl ) . IsAssignableFrom ( type ) ;
if ( ! isDeviceLayout & & ! isControlLayout )
throw new ArgumentException ( $"Types used as layouts have to be InputControls or InputDevices; '{type.Name}' is a '{type.BaseType.Name}'" ,
nameof ( type ) ) ;
var internedName = new InternedString ( name ) ;
var isReplacement = m_Layouts . HasLayout ( internedName ) ;
// All we do is enter the type into a map. We don't construct an InputControlLayout
// from it until we actually need it in an InputDeviceBuilder to create a device.
// This not only avoids us creating a bunch of objects on the managed heap but
// also avoids us laboriously constructing a XRController layout, for example,
// in a game that never uses XR.
m_Layouts . layoutTypes [ internedName ] = type ;
////TODO: make this independent of initialization order
////TODO: re-scan base type information after domain reloads
// Walk class hierarchy all the way up to InputControl to see
// if there's another type that's been registered as a layout.
// If so, make it a base layout for this one.
string baseLayout = null ;
for ( var baseType = type . BaseType ; baseLayout = = null & & baseType ! = typeof ( InputControl ) ;
baseType = baseType . BaseType )
{
foreach ( var entry in m_Layouts . layoutTypes )
if ( entry . Value = = baseType )
{
baseLayout = entry . Key ;
break ;
}
}
PerformLayoutPostRegistration ( internedName , new InlinedArray < InternedString > ( new InternedString ( baseLayout ) ) ,
isReplacement , isKnownToBeDeviceLayout : isDeviceLayout ) ;
}
public void RegisterControlLayout ( string json , string name = null , bool isOverride = false )
{
if ( string . IsNullOrEmpty ( json ) )
throw new ArgumentNullException ( nameof ( json ) ) ;
////REVIEW: as long as no one has instantiated the layout, the base layout information is kinda pointless
// Parse out name, device description, and base layout.
InputControlLayout . ParseHeaderFieldsFromJson ( json , out var nameFromJson , out var baseLayouts ,
out var deviceMatcher ) ;
// Decide whether to take name from JSON or from code.
var internedLayoutName = new InternedString ( name ) ;
if ( internedLayoutName . IsEmpty ( ) )
{
internedLayoutName = nameFromJson ;
// Make sure we have a name.
if ( internedLayoutName . IsEmpty ( ) )
throw new ArgumentException ( "Layout name has not been given and is not set in JSON layout" ,
nameof ( name ) ) ;
}
// If it's an override, it must have a layout the overrides apply to.
if ( isOverride & & baseLayouts . length = = 0 )
{
throw new ArgumentException (
$"Layout override '{internedLayoutName}' must have 'extend' property mentioning layout to which to apply the overrides" ,
nameof ( json ) ) ;
}
// Add it to our records.
var isReplacement = m_Layouts . HasLayout ( internedLayoutName ) ;
if ( isReplacement & & isOverride )
{ // Do not allow a layout override to replace a "base layout" by name, but allow layout overrides
// to replace an existing layout override.
// This is required to guarantee that its a hierarchy (directed graph) rather
// than a cyclic graph.
var isReplacingOverride = m_Layouts . layoutOverrideNames . Contains ( internedLayoutName ) ;
if ( ! isReplacingOverride )
{
throw new ArgumentException ( $"Failed to register layout override '{internedLayoutName}'" +
$"since a layout named '{internedLayoutName}' already exist. Layout overrides must " +
$"have unique names with respect to existing layouts." ) ;
}
}
m_Layouts . layoutStrings [ internedLayoutName ] = json ;
if ( isOverride )
{
m_Layouts . layoutOverrideNames . Add ( internedLayoutName ) ;
for ( var i = 0 ; i < baseLayouts . length ; + + i )
{
var baseLayoutName = baseLayouts [ i ] ;
m_Layouts . layoutOverrides . TryGetValue ( baseLayoutName , out var overrideList ) ;
if ( ! isReplacement )
ArrayHelpers . Append ( ref overrideList , internedLayoutName ) ;
m_Layouts . layoutOverrides [ baseLayoutName ] = overrideList ;
}
}
PerformLayoutPostRegistration ( internedLayoutName , baseLayouts ,
isReplacement : isReplacement , isOverride : isOverride ) ;
// If the layout contained a device matcher, register it.
if ( ! deviceMatcher . empty )
RegisterControlLayoutMatcher ( internedLayoutName , deviceMatcher ) ;
}
public void RegisterControlLayoutBuilder ( Func < InputControlLayout > method , string name ,
string baseLayout = null )
{
if ( method = = null )
throw new ArgumentNullException ( nameof ( method ) ) ;
if ( string . IsNullOrEmpty ( name ) )
throw new ArgumentNullException ( nameof ( name ) ) ;
var internedLayoutName = new InternedString ( name ) ;
var internedBaseLayoutName = new InternedString ( baseLayout ) ;
var isReplacement = m_Layouts . HasLayout ( internedLayoutName ) ;
m_Layouts . layoutBuilders [ internedLayoutName ] = method ;
PerformLayoutPostRegistration ( internedLayoutName , new InlinedArray < InternedString > ( internedBaseLayoutName ) ,
isReplacement ) ;
}
private void PerformLayoutPostRegistration ( InternedString layoutName , InlinedArray < InternedString > baseLayouts ,
bool isReplacement , bool isKnownToBeDeviceLayout = false , bool isOverride = false )
{
+ + m_LayoutRegistrationVersion ;
// Force-clear layout cache. Don't clear reference count so that
// the cache gets cleared out properly when released in case someone
// is using it ATM.
InputControlLayout . s_CacheInstance . Clear ( ) ;
// For layouts that aren't overrides, add the name of the base
// layout to the lookup table.
if ( ! isOverride & & baseLayouts . length > 0 )
{
if ( baseLayouts . length > 1 )
throw new NotSupportedException (
$"Layout '{layoutName}' has multiple base layouts; this is only supported on layout overrides" ) ;
var baseLayoutName = baseLayouts [ 0 ] ;
if ( ! baseLayoutName . IsEmpty ( ) )
m_Layouts . baseLayoutTable [ layoutName ] = baseLayoutName ;
}
// Nuke any precompiled layouts that are invalidated by the layout registration.
m_Layouts . precompiledLayouts . Remove ( layoutName ) ;
if ( m_Layouts . precompiledLayouts . Count > 0 )
{
foreach ( var layout in m_Layouts . precompiledLayouts . Keys . ToArray ( ) )
{
var metadata = m_Layouts . precompiledLayouts [ layout ] . metadata ;
// If it's an override, we remove any precompiled layouts to which overrides are applied.
if ( isOverride )
{
for ( var i = 0 ; i < baseLayouts . length ; + + i )
if ( layout = = baseLayouts [ i ] | |
StringHelpers . CharacterSeparatedListsHaveAtLeastOneCommonElement ( metadata ,
baseLayouts [ i ] , ';' ) )
m_Layouts . precompiledLayouts . Remove ( layout ) ;
}
else
{
// Otherwise, we remove any precompile layouts that use the layout we just changed.
if ( StringHelpers . CharacterSeparatedListsHaveAtLeastOneCommonElement ( metadata ,
layoutName , ';' ) )
m_Layouts . precompiledLayouts . Remove ( layout ) ;
}
}
}
// Recreate any devices using the layout. If it's an override, recreate devices using any of the base layouts.
if ( isOverride )
{
for ( var i = 0 ; i < baseLayouts . length ; + + i )
RecreateDevicesUsingLayout ( baseLayouts [ i ] , isKnownToBeDeviceLayout : isKnownToBeDeviceLayout ) ;
}
else
{
RecreateDevicesUsingLayout ( layoutName , isKnownToBeDeviceLayout : isKnownToBeDeviceLayout ) ;
}
// In the editor, layouts may become available successively after a domain reload so
// we may end up retaining device information all the way until we run the first full
// player update. For every layout we register, we check here whether we have a saved
// device state using a layout with the same name but not having a device description
// (the latter is important as in that case, we should go through the normal matching
// process and not just rely on the name of the layout). If so, we try here to recreate
// the device with the just registered layout.
#if UNITY_EDITOR
for ( var i = 0 ; i < m_SavedDeviceStates . LengthSafe ( ) ; + + i )
{
ref var deviceState = ref m_SavedDeviceStates [ i ] ;
if ( layoutName ! = deviceState . layout | | ! deviceState . description . empty )
continue ;
if ( RestoreDeviceFromSavedState ( ref deviceState , layoutName ) )
{
ArrayHelpers . EraseAt ( ref m_SavedDeviceStates , i ) ;
- - i ;
}
}
#endif
// Let listeners know.
var change = isReplacement ? InputControlLayoutChange . Replaced : InputControlLayoutChange . Added ;
DelegateHelpers . InvokeCallbacksSafe ( ref m_LayoutChangeListeners , layoutName . ToString ( ) , change , "InputSystem.onLayoutChange" ) ;
}
public void RegisterPrecompiledLayout < TDevice > ( string metadata )
where TDevice : InputDevice , new ( )
{
if ( metadata = = null )
throw new ArgumentNullException ( nameof ( metadata ) ) ;
var deviceType = typeof ( TDevice ) . BaseType ;
var layoutName = FindOrRegisterDeviceLayoutForType ( deviceType ) ;
m_Layouts . precompiledLayouts [ layoutName ] = new InputControlLayout . Collection . PrecompiledLayout
{
factoryMethod = ( ) = > new TDevice ( ) ,
metadata = metadata
} ;
}
private void RecreateDevicesUsingLayout ( InternedString layout , bool isKnownToBeDeviceLayout = false )
{
if ( m_DevicesCount = = 0 )
return ;
List < InputDevice > devicesUsingLayout = null ;
// Find all devices using the layout.
for ( var i = 0 ; i < m_DevicesCount ; + + i )
{
var device = m_Devices [ i ] ;
bool usesLayout ;
if ( isKnownToBeDeviceLayout )
usesLayout = IsControlUsingLayout ( device , layout ) ;
else
usesLayout = IsControlOrChildUsingLayoutRecursive ( device , layout ) ;
if ( usesLayout )
{
if ( devicesUsingLayout = = null )
devicesUsingLayout = new List < InputDevice > ( ) ;
devicesUsingLayout . Add ( device ) ;
}
}
// If there's none, we're good.
if ( devicesUsingLayout = = null )
return ;
// Remove and re-add the matching devices.
using ( InputDeviceBuilder . Ref ( ) )
{
for ( var i = 0 ; i < devicesUsingLayout . Count ; + + i )
{
var device = devicesUsingLayout [ i ] ;
RecreateDevice ( device , device . m_Layout ) ;
}
}
}
private bool IsControlOrChildUsingLayoutRecursive ( InputControl control , InternedString layout )
{
// Check control itself.
if ( IsControlUsingLayout ( control , layout ) )
return true ;
// Check children.
var children = control . children ;
for ( var i = 0 ; i < children . Count ; + + i )
if ( IsControlOrChildUsingLayoutRecursive ( children [ i ] , layout ) )
return true ;
return false ;
}
private bool IsControlUsingLayout ( InputControl control , InternedString layout )
{
// Check direct match.
if ( control . layout = = layout )
return true ;
// Check base layout chain.
var baseLayout = control . m_Layout ;
while ( m_Layouts . baseLayoutTable . TryGetValue ( baseLayout , out baseLayout ) )
if ( baseLayout = = layout )
return true ;
return false ;
}
public void RegisterControlLayoutMatcher ( string layoutName , InputDeviceMatcher matcher )
{
if ( string . IsNullOrEmpty ( layoutName ) )
throw new ArgumentNullException ( nameof ( layoutName ) ) ;
if ( matcher . empty )
throw new ArgumentException ( "Matcher cannot be empty" , nameof ( matcher ) ) ;
// Add to table.
var internedLayoutName = new InternedString ( layoutName ) ;
m_Layouts . AddMatcher ( internedLayoutName , matcher ) ;
// Recreate any device that we match better than its current layout.
RecreateDevicesUsingLayoutWithInferiorMatch ( matcher ) ;
// See if we can make sense of any device we couldn't make sense of before.
AddAvailableDevicesMatchingDescription ( matcher , internedLayoutName ) ;
}
public void RegisterControlLayoutMatcher ( Type type , InputDeviceMatcher matcher )
{
if ( type = = null )
throw new ArgumentNullException ( nameof ( type ) ) ;
if ( matcher . empty )
throw new ArgumentException ( "Matcher cannot be empty" , nameof ( matcher ) ) ;
var layoutName = m_Layouts . TryFindLayoutForType ( type ) ;
if ( layoutName . IsEmpty ( ) )
throw new ArgumentException (
$"Type '{type.Name}' has not been registered as a control layout" , nameof ( type ) ) ;
RegisterControlLayoutMatcher ( layoutName , matcher ) ;
}
private void RecreateDevicesUsingLayoutWithInferiorMatch ( InputDeviceMatcher deviceMatcher )
{
if ( m_DevicesCount = = 0 )
return ;
using ( InputDeviceBuilder . Ref ( ) )
{
var deviceCount = m_DevicesCount ;
for ( var i = 0 ; i < deviceCount ; + + i )
{
var device = m_Devices [ i ] ;
var deviceDescription = device . description ;
if ( deviceDescription . empty | | ! ( deviceMatcher . MatchPercentage ( deviceDescription ) > 0 ) )
continue ;
var layoutName = TryFindMatchingControlLayout ( ref deviceDescription , device . deviceId ) ;
if ( layoutName ! = device . m_Layout )
{
device . m_Description = deviceDescription ;
RecreateDevice ( device , layoutName ) ;
// We're removing devices in the middle of the array and appending
// them at the end. Adjust our index and device count to make sure
// we're not iterating all the way into already processed devices.
- - i ;
- - deviceCount ;
}
}
}
}
private void RecreateDevice ( InputDevice oldDevice , InternedString newLayout )
{
// Remove.
RemoveDevice ( oldDevice , keepOnListOfAvailableDevices : true ) ;
// Re-setup device.
var newDevice = InputDevice . Build < InputDevice > ( newLayout , oldDevice . m_Variants ,
deviceDescription : oldDevice . m_Description ) ;
// Preserve device properties that should not be changed by the re-creation
// of a device.
newDevice . m_DeviceId = oldDevice . m_DeviceId ;
newDevice . m_Description = oldDevice . m_Description ;
if ( oldDevice . native )
newDevice . m_DeviceFlags | = InputDevice . DeviceFlags . Native ;
if ( oldDevice . remote )
newDevice . m_DeviceFlags | = InputDevice . DeviceFlags . Remote ;
if ( ! oldDevice . enabled )
{
newDevice . m_DeviceFlags | = InputDevice . DeviceFlags . DisabledStateHasBeenQueriedFromRuntime ;
newDevice . m_DeviceFlags | = InputDevice . DeviceFlags . DisabledInFrontend ;
}
// Re-add.
AddDevice ( newDevice ) ;
}
private void AddAvailableDevicesMatchingDescription ( InputDeviceMatcher matcher , InternedString layout )
{
#if UNITY_EDITOR
// If we still have some devices saved from the last domain reload, see
// if they are matched by the given matcher. If so, turn them into devices.
for ( var i = 0 ; i < m_SavedDeviceStates . LengthSafe ( ) ; + + i )
{
ref var deviceState = ref m_SavedDeviceStates [ i ] ;
if ( matcher . MatchPercentage ( deviceState . description ) > 0 )
{
RestoreDeviceFromSavedState ( ref deviceState , layout ) ;
ArrayHelpers . EraseAt ( ref m_SavedDeviceStates , i ) ;
- - i ;
}
}
#endif
// See if the new description to layout mapping allows us to make
// sense of a device we couldn't make sense of so far.
for ( var i = 0 ; i < m_AvailableDeviceCount ; + + i )
{
// Ignore if it's a device that has been explicitly removed.
if ( m_AvailableDevices [ i ] . isRemoved )
continue ;
var deviceId = m_AvailableDevices [ i ] . deviceId ;
if ( TryGetDeviceById ( deviceId ) ! = null )
continue ;
if ( matcher . MatchPercentage ( m_AvailableDevices [ i ] . description ) > 0f )
{
// Try to create InputDevice instance.
try
{
AddDevice ( layout , deviceId , deviceDescription : m_AvailableDevices [ i ] . description ,
deviceFlags : m_AvailableDevices [ i ] . isNative ? InputDevice . DeviceFlags . Native : 0 ) ;
}
catch ( Exception exception )
{
Debug . LogError (
$"Layout '{layout}' matches existing device '{m_AvailableDevices[i].description}' but failed to instantiate: {exception}" ) ;
Debug . LogException ( exception ) ;
continue ;
}
// Re-enable device.
var command = EnableDeviceCommand . Create ( ) ;
m_Runtime . DeviceCommand ( deviceId , ref command ) ;
}
}
}
public void RemoveControlLayout ( string name )
{
if ( string . IsNullOrEmpty ( name ) )
throw new ArgumentNullException ( nameof ( name ) ) ;
var internedName = new InternedString ( name ) ;
// Remove all devices using the layout.
for ( var i = 0 ; i < m_DevicesCount ; )
{
var device = m_Devices [ i ] ;
if ( IsControlOrChildUsingLayoutRecursive ( device , internedName ) )
{
RemoveDevice ( device , keepOnListOfAvailableDevices : true ) ;
}
else
{
+ + i ;
}
}
// Remove layout record.
m_Layouts . layoutTypes . Remove ( internedName ) ;
m_Layouts . layoutStrings . Remove ( internedName ) ;
m_Layouts . layoutBuilders . Remove ( internedName ) ;
m_Layouts . baseLayoutTable . Remove ( internedName ) ;
+ + m_LayoutRegistrationVersion ;
////TODO: check all layout inheritance chain for whether they are based on the layout and if so
//// remove those layouts, too
// Let listeners know.
DelegateHelpers . InvokeCallbacksSafe ( ref m_LayoutChangeListeners , name , InputControlLayoutChange . Removed , "InputSystem.onLayoutChange" ) ;
}
public InputControlLayout TryLoadControlLayout ( Type type )
{
if ( type = = null )
throw new ArgumentNullException ( nameof ( type ) ) ;
if ( ! typeof ( InputControl ) . IsAssignableFrom ( type ) )
throw new ArgumentException ( $"Type '{type.Name}' is not an InputControl" , nameof ( type ) ) ;
// Find the layout name that the given type was registered with.
var layoutName = m_Layouts . TryFindLayoutForType ( type ) ;
if ( layoutName . IsEmpty ( ) )
throw new ArgumentException (
$"Type '{type.Name}' has not been registered as a control layout" , nameof ( type ) ) ;
return m_Layouts . TryLoadLayout ( layoutName ) ;
}
public InputControlLayout TryLoadControlLayout ( InternedString name )
{
return m_Layouts . TryLoadLayout ( name ) ;
}
////FIXME: allowing the description to be modified as part of this is surprising; find a better way
public InternedString TryFindMatchingControlLayout ( ref InputDeviceDescription deviceDescription , int deviceId = InputDevice . InvalidDeviceId )
{
Profiler . BeginSample ( "InputSystem.TryFindMatchingControlLayout" ) ;
////TODO: this will want to take overrides into account
// See if we can match by description.
var layoutName = m_Layouts . TryFindMatchingLayout ( deviceDescription ) ;
if ( layoutName . IsEmpty ( ) )
{
// No, so try to match by device class. If we have a "Gamepad" layout,
// for example, a device that classifies itself as a "Gamepad" will match
// that layout.
//
// NOTE: Have to make sure here that we get a device layout and not a
// control layout.
if ( ! string . IsNullOrEmpty ( deviceDescription . deviceClass ) )
{
var deviceClassLowerCase = new InternedString ( deviceDescription . deviceClass ) ;
var type = m_Layouts . GetControlTypeForLayout ( deviceClassLowerCase ) ;
if ( type ! = null & & typeof ( InputDevice ) . IsAssignableFrom ( type ) )
layoutName = new InternedString ( deviceDescription . deviceClass ) ;
}
}
////REVIEW: listeners registering new layouts from in here may potentially lead to the creation of devices; should we disallow that?
////REVIEW: if a callback picks a layout, should we re-run through the list of callbacks? or should we just remove haveOverridenLayoutName?
// Give listeners a shot to select/create a layout.
if ( m_DeviceFindLayoutCallbacks . length > 0 )
{
// First time we get here, put our delegate for executing device commands
// in place. We wrap the call to IInputRuntime.DeviceCommand so that we don't
// need to expose the runtime to the onFindLayoutForDevice callbacks.
if ( m_DeviceFindExecuteCommandDelegate = = null )
m_DeviceFindExecuteCommandDelegate =
( ref InputDeviceCommand commandRef ) = >
{
if ( m_DeviceFindExecuteCommandDeviceId = = InputDevice . InvalidDeviceId )
return InputDeviceCommand . GenericFailure ;
return m_Runtime . DeviceCommand ( m_DeviceFindExecuteCommandDeviceId , ref commandRef ) ;
} ;
m_DeviceFindExecuteCommandDeviceId = deviceId ;
var haveOverriddenLayoutName = false ;
m_DeviceFindLayoutCallbacks . LockForChanges ( ) ;
for ( var i = 0 ; i < m_DeviceFindLayoutCallbacks . length ; + + i )
{
try
{
var newLayout = m_DeviceFindLayoutCallbacks [ i ] ( ref deviceDescription , layoutName , m_DeviceFindExecuteCommandDelegate ) ;
if ( ! string . IsNullOrEmpty ( newLayout ) & & ! haveOverriddenLayoutName )
{
layoutName = new InternedString ( newLayout ) ;
haveOverriddenLayoutName = true ;
}
}
catch ( Exception exception )
{
Debug . LogError ( $"{exception.GetType().Name} while executing 'InputSystem.onFindLayoutForDevice' callbacks" ) ;
Debug . LogException ( exception ) ;
}
}
m_DeviceFindLayoutCallbacks . UnlockForChanges ( ) ;
}
Profiler . EndSample ( ) ;
return layoutName ;
}
private InternedString FindOrRegisterDeviceLayoutForType ( Type type )
{
var layoutName = m_Layouts . TryFindLayoutForType ( type ) ;
if ( layoutName . IsEmpty ( ) )
{
// Automatically register the given type as a layout.
if ( layoutName . IsEmpty ( ) )
{
layoutName = new InternedString ( type . Name ) ;
RegisterControlLayout ( type . Name , type ) ;
}
}
return layoutName ;
}
/// <summary>
/// Return true if the given device layout is supported by the game according to <see cref="InputSettings.supportedDevices"/>.
/// </summary>
/// <param name="layoutName">Name of the device layout.</param>
/// <returns>True if a device with the given layout should be created for the game, false otherwise.</returns>
private bool IsDeviceLayoutMarkedAsSupportedInSettings ( InternedString layoutName )
{
// In the editor, "Supported Devices" can be overridden by a user setting. This causes
// all available devices to be added regardless of what "Supported Devices" says. This
// is useful to ensure that things like keyboard, mouse, and pen keep working in the editor
// even if not supported as devices in the game.
#if UNITY_EDITOR
if ( InputEditorUserSettings . addDevicesNotSupportedByProject )
return true ;
#endif
var supportedDevices = m_Settings . supportedDevices ;
if ( supportedDevices . Count = = 0 )
{
// If supportedDevices is empty, all device layouts are considered supported.
return true ;
}
for ( var n = 0 ; n < supportedDevices . Count ; + + n )
{
var supportedLayout = new InternedString ( supportedDevices [ n ] ) ;
if ( layoutName = = supportedLayout | | m_Layouts . IsBasedOn ( supportedLayout , layoutName ) )
return true ;
}
return false ;
}
public IEnumerable < string > ListControlLayouts ( string basedOn = null )
{
////FIXME: this may add a name twice
if ( ! string . IsNullOrEmpty ( basedOn ) )
{
var internedBasedOn = new InternedString ( basedOn ) ;
foreach ( var entry in m_Layouts . layoutTypes )
if ( m_Layouts . IsBasedOn ( internedBasedOn , entry . Key ) )
yield return entry . Key ;
foreach ( var entry in m_Layouts . layoutStrings )
if ( m_Layouts . IsBasedOn ( internedBasedOn , entry . Key ) )
yield return entry . Key ;
foreach ( var entry in m_Layouts . layoutBuilders )
if ( m_Layouts . IsBasedOn ( internedBasedOn , entry . Key ) )
yield return entry . Key ;
}
else
{
foreach ( var entry in m_Layouts . layoutTypes )
yield return entry . Key ;
foreach ( var entry in m_Layouts . layoutStrings )
yield return entry . Key ;
foreach ( var entry in m_Layouts . layoutBuilders )
yield return entry . Key ;
}
}
// Adds all controls that match the given path spec to the given list.
// Returns number of controls added to the list.
// NOTE: Does not create garbage.
/// <summary>
/// Adds to the given list all controls that match the given <see cref="InputControlPath">path spec</see>
/// and are assignable to the given type.
/// </summary>
/// <param name="path"></param>
/// <param name="controls"></param>
/// <typeparam name="TControl"></typeparam>
/// <returns></returns>
public int GetControls < TControl > ( string path , ref InputControlList < TControl > controls )
where TControl : InputControl
{
if ( string . IsNullOrEmpty ( path ) )
return 0 ;
if ( m_DevicesCount = = 0 )
return 0 ;
var deviceCount = m_DevicesCount ;
var numMatches = 0 ;
for ( var i = 0 ; i < deviceCount ; + + i )
{
var device = m_Devices [ i ] ;
numMatches + = InputControlPath . TryFindControls ( device , path , 0 , ref controls ) ;
}
return numMatches ;
}
public void SetDeviceUsage ( InputDevice device , InternedString usage )
{
if ( device = = null )
throw new ArgumentNullException ( nameof ( device ) ) ;
if ( device . usages . Count = = 1 & & device . usages [ 0 ] = = usage )
return ;
if ( device . usages . Count = = 0 & & usage . IsEmpty ( ) )
return ;
device . ClearDeviceUsages ( ) ;
if ( ! usage . IsEmpty ( ) )
device . AddDeviceUsage ( usage ) ;
NotifyUsageChanged ( device ) ;
}
public void AddDeviceUsage ( InputDevice device , InternedString usage )
{
if ( device = = null )
throw new ArgumentNullException ( nameof ( device ) ) ;
if ( usage . IsEmpty ( ) )
throw new ArgumentException ( "Usage string cannot be empty" , nameof ( usage ) ) ;
if ( device . usages . Contains ( usage ) )
return ;
device . AddDeviceUsage ( usage ) ;
NotifyUsageChanged ( device ) ;
}
public void RemoveDeviceUsage ( InputDevice device , InternedString usage )
{
if ( device = = null )
throw new ArgumentNullException ( nameof ( device ) ) ;
if ( usage . IsEmpty ( ) )
throw new ArgumentException ( "Usage string cannot be empty" , nameof ( usage ) ) ;
if ( ! device . usages . Contains ( usage ) )
return ;
device . RemoveDeviceUsage ( usage ) ;
NotifyUsageChanged ( device ) ;
}
private void NotifyUsageChanged ( InputDevice device )
{
InputActionState . OnDeviceChange ( device , InputDeviceChange . UsageChanged ) ;
// Notify listeners.
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceChangeListeners , device , InputDeviceChange . UsageChanged , "InputSystem.onDeviceChange" ) ;
////REVIEW: This was for the XRController leftHand and rightHand getters but these do lookups dynamically now; remove?
// Usage may affect current device so update.
device . MakeCurrent ( ) ;
}
////TODO: make sure that no device or control with a '/' in the name can creep into the system
public InputDevice AddDevice ( Type type , string name = null )
{
if ( type = = null )
throw new ArgumentNullException ( nameof ( type ) ) ;
// Find the layout name that the given type was registered with.
var layoutName = FindOrRegisterDeviceLayoutForType ( type ) ;
Debug . Assert ( ! layoutName . IsEmpty ( ) , name ) ;
// Note that since we go through the normal by-name lookup here, this will
// still work if the layout from the type was override with a string layout.
return AddDevice ( layoutName , name ) ;
}
// Creates a device from the given layout and adds it to the system.
// NOTE: Creates garbage.
public InputDevice AddDevice ( string layout , string name = null , InternedString variants = new InternedString ( ) )
{
if ( string . IsNullOrEmpty ( layout ) )
throw new ArgumentNullException ( nameof ( layout ) ) ;
var device = InputDevice . Build < InputDevice > ( layout , variants ) ;
if ( ! string . IsNullOrEmpty ( name ) )
device . m_Name = new InternedString ( name ) ;
AddDevice ( device ) ;
return device ;
}
// Add device with a forced ID. Used when creating devices reported to us by native.
private InputDevice AddDevice ( InternedString layout , int deviceId ,
string deviceName = null ,
InputDeviceDescription deviceDescription = new InputDeviceDescription ( ) ,
InputDevice . DeviceFlags deviceFlags = 0 ,
InternedString variants = default )
{
var device = InputDevice . Build < InputDevice > ( new InternedString ( layout ) ,
deviceDescription : deviceDescription ,
layoutVariants : variants ) ;
device . m_DeviceId = deviceId ;
device . m_Description = deviceDescription ;
device . m_DeviceFlags | = deviceFlags ;
if ( ! string . IsNullOrEmpty ( deviceName ) )
device . m_Name = new InternedString ( deviceName ) ;
// Default display name to product name.
if ( ! string . IsNullOrEmpty ( deviceDescription . product ) )
device . m_DisplayName = deviceDescription . product ;
AddDevice ( device ) ;
return device ;
}
public void AddDevice ( InputDevice device )
{
if ( device = = null )
throw new ArgumentNullException ( nameof ( device ) ) ;
if ( string . IsNullOrEmpty ( device . layout ) )
throw new InvalidOperationException ( "Device has no associated layout" ) ;
// Ignore if the same device gets added multiple times.
if ( ArrayHelpers . Contains ( m_Devices , device ) )
return ;
MakeDeviceNameUnique ( device ) ;
AssignUniqueDeviceId ( device ) ;
// Add to list.
device . m_DeviceIndex = ArrayHelpers . AppendWithCapacity ( ref m_Devices , ref m_DevicesCount , device ) ;
////REVIEW: Not sure a full-blown dictionary is the right way here. Alternatives are to keep
//// a sparse array that directly indexes using the linearly increasing IDs (though that
//// may get large over time). Or to just do a linear search through m_Devices (but
//// that may end up tapping a bunch of memory locations in the heap to find the right
//// device; could be improved by sorting m_Devices by ID and picking a good starting
//// point based on the ID we have instead of searching from [0] always).
m_DevicesById [ device . deviceId ] = device ;
// Let InputStateBuffers know this device doesn't have any associated state yet.
device . m_StateBlock . byteOffset = InputStateBlock . InvalidOffset ;
// Update state buffers.
ReallocateStateBuffers ( ) ;
InitializeDeviceState ( device ) ;
// Update metrics.
m_Metrics . maxNumDevices = Mathf . Max ( m_DevicesCount , m_Metrics . maxNumDevices ) ;
m_Metrics . maxStateSizeInBytes = Mathf . Max ( ( int ) m_StateBuffers . totalSize , m_Metrics . maxStateSizeInBytes ) ;
// Make sure that if the device ID is listed in m_AvailableDevices, the device
// is no longer marked as removed.
for ( var i = 0 ; i < m_AvailableDeviceCount ; + + i )
{
if ( m_AvailableDevices [ i ] . deviceId = = device . deviceId )
m_AvailableDevices [ i ] . isRemoved = false ;
}
// If we're running in the background, find out whether the device can run in
// the background. If not, disable it.
var isPlaying = true ;
#if UNITY_EDITOR
isPlaying = m_Runtime . isInPlayMode ;
#endif
if ( isPlaying & & ! gameHasFocus
& & m_Settings . backgroundBehavior ! = InputSettings . BackgroundBehavior . IgnoreFocus
& & m_Runtime . runInBackground
& & device . QueryEnabledStateFromRuntime ( )
& & ! ShouldRunDeviceInBackground ( device ) )
{
EnableOrDisableDevice ( device , false , DeviceDisableScope . TemporaryWhilePlayerIsInBackground ) ;
}
////REVIEW: we may want to suppress this during the initial device discovery phase
// Let actions re-resolve their paths.
InputActionState . OnDeviceChange ( device , InputDeviceChange . Added ) ;
// If the device wants automatic callbacks before input updates,
// put it on the list.
if ( device is IInputUpdateCallbackReceiver beforeUpdateCallbackReceiver )
onBeforeUpdate + = beforeUpdateCallbackReceiver . OnUpdate ;
// If the device has state callbacks, make a note of it.
if ( device is IInputStateCallbackReceiver )
{
InstallBeforeUpdateHookIfNecessary ( ) ;
device . m_DeviceFlags | = InputDevice . DeviceFlags . HasStateCallbacks ;
m_HaveDevicesWithStateCallbackReceivers = true ;
}
// If the device has event merger, make a note of it.
if ( device is IEventMerger )
device . hasEventMerger = true ;
// If the device has event preprocessor, make a note of it.
if ( device is IEventPreProcessor )
device . hasEventPreProcessor = true ;
// If the device wants before-render updates, enable them if they
// aren't already.
if ( device . updateBeforeRender )
updateMask | = InputUpdateType . BeforeRender ;
// Notify device.
device . NotifyAdded ( ) ;
////REVIEW: is this really a good thing to do? just plugging in a device shouldn't make
//// it current, no?
// Make the device current.
// BEWARE: if this will not happen for whatever reason, you will break Android sensors,
// as they rely on .current for enabling native backend, see https://fogbugz.unity3d.com/f/cases/1371204/
device . MakeCurrent ( ) ;
// Notify listeners.
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceChangeListeners , device , InputDeviceChange . Added , "InputSystem.onDeviceChange" ) ;
// Request device to send us an initial state update.
if ( device . enabled )
device . RequestSync ( ) ;
2023-05-07 18:43:11 -04:00
device . SetOptimizedControlDataTypeRecursively ( ) ;
2023-03-28 13:24:16 -04:00
}
////TODO: this path should really put the device on the list of available devices
////TODO: this path should discover disconnected devices
public InputDevice AddDevice ( InputDeviceDescription description )
{
////REVIEW: is throwing here really such a useful thing?
return AddDevice ( description , throwIfNoLayoutFound : true ) ;
}
public InputDevice AddDevice ( InputDeviceDescription description , bool throwIfNoLayoutFound ,
string deviceName = null , int deviceId = InputDevice . InvalidDeviceId , InputDevice . DeviceFlags deviceFlags = 0 )
{
Profiler . BeginSample ( "InputSystem.AddDevice" ) ;
// Look for matching layout.
var layout = TryFindMatchingControlLayout ( ref description , deviceId ) ;
// If no layout was found, bail out.
if ( layout . IsEmpty ( ) )
{
if ( throwIfNoLayoutFound )
throw new ArgumentException ( $"Cannot find layout matching device description '{description}'" , nameof ( description ) ) ;
// If it's a device coming from the runtime, disable it.
if ( deviceId ! = InputDevice . InvalidDeviceId )
{
var command = DisableDeviceCommand . Create ( ) ;
m_Runtime . DeviceCommand ( deviceId , ref command ) ;
}
Profiler . EndSample ( ) ;
return null ;
}
var device = AddDevice ( layout , deviceId , deviceName , description , deviceFlags ) ;
device . m_Description = description ;
Profiler . EndSample ( ) ;
return device ;
}
public InputDevice AddDevice ( InputDeviceDescription description , InternedString layout , string deviceName = null ,
int deviceId = InputDevice . InvalidDeviceId , InputDevice . DeviceFlags deviceFlags = 0 )
{
try
{
Profiler . BeginSample ( "InputSystem.AddDevice" ) ;
var device = AddDevice ( layout , deviceId , deviceName , description , deviceFlags ) ;
device . m_Description = description ;
return device ;
}
finally
{
Profiler . EndSample ( ) ;
}
}
public void RemoveDevice ( InputDevice device , bool keepOnListOfAvailableDevices = false )
{
if ( device = = null )
throw new ArgumentNullException ( nameof ( device ) ) ;
// If device has not been added, ignore.
if ( device . m_DeviceIndex = = InputDevice . kInvalidDeviceIndex )
return ;
// Remove state monitors while device index is still valid.
RemoveStateChangeMonitors ( device ) ;
// Remove from device array.
var deviceIndex = device . m_DeviceIndex ;
var deviceId = device . deviceId ;
if ( deviceIndex < m_StateChangeMonitors . LengthSafe ( ) )
{
// m_StateChangeMonitors mirrors layout of m_Devices *but* may be shorter.
var count = m_StateChangeMonitors . Length ;
ArrayHelpers . EraseAtWithCapacity ( m_StateChangeMonitors , ref count , deviceIndex ) ;
}
ArrayHelpers . EraseAtWithCapacity ( m_Devices , ref m_DevicesCount , deviceIndex ) ;
m_DevicesById . Remove ( deviceId ) ;
if ( m_Devices ! = null )
{
// Remove from state buffers.
ReallocateStateBuffers ( ) ;
}
else
{
// No more devices. Kill state buffers.
m_StateBuffers . FreeAll ( ) ;
}
////TODO: When we remove a native device like this, make sure we tell the backend to disable it (and re-enable it when re-add it)
// Update device indices. Do this after reallocating state buffers as that call requires
// the old indices to still be in place.
for ( var i = deviceIndex ; i < m_DevicesCount ; + + i )
- - m_Devices [ i ] . m_DeviceIndex ; // Indices have shifted down by one.
device . m_DeviceIndex = InputDevice . kInvalidDeviceIndex ;
// Update list of available devices.
for ( var i = 0 ; i < m_AvailableDeviceCount ; + + i )
{
if ( m_AvailableDevices [ i ] . deviceId = = deviceId )
{
if ( keepOnListOfAvailableDevices )
m_AvailableDevices [ i ] . isRemoved = true ;
else
ArrayHelpers . EraseAtWithCapacity ( m_AvailableDevices , ref m_AvailableDeviceCount , i ) ;
break ;
}
}
// Unbake offset into global state buffers.
device . BakeOffsetIntoStateBlockRecursive ( ( uint ) - device . m_StateBlock . byteOffset ) ;
// Force enabled actions to remove controls from the device.
// We've already set the device index to be invalid so we any attempts
// by actions to uninstall state monitors will get ignored.
InputActionState . OnDeviceChange ( device , InputDeviceChange . Removed ) ;
// Kill before update callback, if applicable.
if ( device is IInputUpdateCallbackReceiver beforeUpdateCallbackReceiver )
onBeforeUpdate - = beforeUpdateCallbackReceiver . OnUpdate ;
// Disable before-render updates if this was the last device
// that requires them.
if ( device . updateBeforeRender )
{
var haveDeviceRequiringBeforeRender = false ;
for ( var i = 0 ; i < m_DevicesCount ; + + i )
if ( m_Devices [ i ] . updateBeforeRender )
{
haveDeviceRequiringBeforeRender = true ;
break ;
}
if ( ! haveDeviceRequiringBeforeRender )
updateMask & = ~ InputUpdateType . BeforeRender ;
}
// Let device know.
device . NotifyRemoved ( ) ;
// Let listeners know.
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceChangeListeners , device , InputDeviceChange . Removed , "InputSystem.onDeviceChange" ) ;
// Try setting next device of same type as current
InputSystem . GetDevice ( device . GetType ( ) ) ? . MakeCurrent ( ) ;
}
public void FlushDisconnectedDevices ( )
{
m_DisconnectedDevices . Clear ( m_DisconnectedDevicesCount ) ;
m_DisconnectedDevicesCount = 0 ;
}
public unsafe void ResetDevice ( InputDevice device , bool alsoResetDontResetControls = false , bool? issueResetCommand = null )
{
if ( device = = null )
throw new ArgumentNullException ( nameof ( device ) ) ;
if ( ! device . added )
throw new InvalidOperationException ( $"Device '{device}' has not been added to the system" ) ;
var isHardReset = alsoResetDontResetControls | | ! device . hasDontResetControls ;
// Trigger reset notification.
var change = isHardReset ? InputDeviceChange . HardReset : InputDeviceChange . SoftReset ;
InputActionState . OnDeviceChange ( device , change ) ;
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceChangeListeners , device , change , "onDeviceChange" ) ;
// If the device implements its own reset, let it handle it.
if ( ! alsoResetDontResetControls & & device is ICustomDeviceReset customReset )
{
customReset . Reset ( ) ;
}
else
{
var defaultStatePtr = device . defaultStatePtr ;
var deviceStateBlockSize = device . stateBlock . alignedSizeInBytes ;
// Allocate temp memory to hold one state event.
////REVIEW: the need for an event here is sufficiently obscure to warrant scrutiny; likely, there's a better way
//// to tell synthetic input (or input sources in general) apart
// NOTE: We wrap the reset in an artificial state event so that it appears to the rest of the system
// like any other input. If we don't do that but rather just call UpdateState() with a null event
// pointer, the change will be considered an internal state change and will get ignored by some
// pieces of code (such as EnhancedTouch which filters out internal state changes of Touchscreen
// by ignoring any change that is not coming from an input event).
using ( var tempBuffer =
new NativeArray < byte > ( InputEvent . kBaseEventSize + sizeof ( int ) + ( int ) deviceStateBlockSize , Allocator . Temp ) )
{
var stateEventPtr = ( StateEvent * ) tempBuffer . GetUnsafePtr ( ) ;
var statePtr = stateEventPtr - > state ;
var currentTime = m_Runtime . currentTime ;
// Set up the state event.
ref var stateBlock = ref device . m_StateBlock ;
stateEventPtr - > baseEvent . type = StateEvent . Type ;
stateEventPtr - > baseEvent . sizeInBytes = InputEvent . kBaseEventSize + sizeof ( int ) + deviceStateBlockSize ;
stateEventPtr - > baseEvent . time = currentTime ;
stateEventPtr - > baseEvent . deviceId = device . deviceId ;
stateEventPtr - > baseEvent . eventId = - 1 ;
stateEventPtr - > stateFormat = device . m_StateBlock . format ;
// Decide whether we perform a soft reset or a hard reset.
if ( isHardReset )
{
// Perform a hard reset where we wipe the entire device and set a full
// reset request to the backend.
UnsafeUtility . MemCpy ( statePtr ,
( byte * ) defaultStatePtr + stateBlock . byteOffset ,
deviceStateBlockSize ) ;
}
else
{
// Perform a soft reset where we exclude any dontReset control (which is automatically
// toggled on for noisy controls) and do *NOT* send a reset request to the backend.
var currentStatePtr = device . currentStatePtr ;
var resetMaskPtr = m_StateBuffers . resetMaskBuffer ;
// To preserve values from dontReset controls, we need to first copy their current values.
UnsafeUtility . MemCpy ( statePtr ,
( byte * ) currentStatePtr + stateBlock . byteOffset ,
deviceStateBlockSize ) ;
// And then we copy over default values masked by dontReset bits.
MemoryHelpers . MemCpyMasked ( statePtr ,
( byte * ) defaultStatePtr + stateBlock . byteOffset ,
( int ) deviceStateBlockSize ,
( byte * ) resetMaskPtr + stateBlock . byteOffset ) ;
}
UpdateState ( device , defaultUpdateType , statePtr , 0 , deviceStateBlockSize , currentTime ,
new InputEventPtr ( ( InputEvent * ) stateEventPtr ) ) ;
}
}
// In the editor, we don't want to issue RequestResetCommand to devices based on focus of the game view
// as this would also reset device state for the editor. And we don't need the reset commands in this case
// as -- unlike in the player --, Unity keeps running and we will keep seeing OS messages for these devices.
// So, in the editor, we generally suppress reset commands.
//
// The only exception is when the editor itself loses focus. We issue sync requests to all devices when
// coming back into focus. But for any device that doesn't support syncs, we actually do want to have a
// reset command reach the background.
//
// Finally, in the player, we also avoid reset commands when disabling a device as these are pointless.
// We sync/reset when enabling a device in the backend.
var doIssueResetCommand = isHardReset ;
if ( issueResetCommand ! = null )
doIssueResetCommand = issueResetCommand . Value ;
#if UNITY_EDITOR
else if ( m_Settings . editorInputBehaviorInPlayMode ! = InputSettings . EditorInputBehaviorInPlayMode . AllDeviceInputAlwaysGoesToGameView )
doIssueResetCommand = false ;
#endif
if ( doIssueResetCommand )
device . RequestReset ( ) ;
}
public InputDevice TryGetDevice ( string nameOrLayout )
{
if ( string . IsNullOrEmpty ( nameOrLayout ) )
throw new ArgumentException ( "Name is null or empty." , nameof ( nameOrLayout ) ) ;
if ( m_DevicesCount = = 0 )
return null ;
var nameOrLayoutLowerCase = nameOrLayout . ToLower ( ) ;
for ( var i = 0 ; i < m_DevicesCount ; + + i )
{
var device = m_Devices [ i ] ;
if ( device . m_Name . ToLower ( ) = = nameOrLayoutLowerCase | |
device . m_Layout . ToLower ( ) = = nameOrLayoutLowerCase )
return device ;
}
return null ;
}
public InputDevice GetDevice ( string nameOrLayout )
{
var device = TryGetDevice ( nameOrLayout ) ;
if ( device = = null )
throw new ArgumentException ( $"Cannot find device with name or layout '{nameOrLayout}'" , nameof ( nameOrLayout ) ) ;
return device ;
}
public InputDevice TryGetDevice ( Type layoutType )
{
var layoutName = m_Layouts . TryFindLayoutForType ( layoutType ) ;
if ( layoutName . IsEmpty ( ) )
return null ;
return TryGetDevice ( layoutName ) ;
}
public InputDevice TryGetDeviceById ( int id )
{
if ( m_DevicesById . TryGetValue ( id , out var result ) )
return result ;
return null ;
}
// Adds any device that's been reported to the system but could not be matched to
// a layout to the given list.
public int GetUnsupportedDevices ( List < InputDeviceDescription > descriptions )
{
if ( descriptions = = null )
throw new ArgumentNullException ( nameof ( descriptions ) ) ;
var numFound = 0 ;
for ( var i = 0 ; i < m_AvailableDeviceCount ; + + i )
{
if ( TryGetDeviceById ( m_AvailableDevices [ i ] . deviceId ) ! = null )
continue ;
descriptions . Add ( m_AvailableDevices [ i ] . description ) ;
+ + numFound ;
}
return numFound ;
}
// We have three different levels of disabling a device.
internal enum DeviceDisableScope
{
Everywhere , // Device is disabled globally and explicitly. Should neither send nor receive events.
InFrontendOnly , // Device is only disabled on managed side but not in backend. Should keep sending events but should not receive them (useful for redirecting their data).
TemporaryWhilePlayerIsInBackground , // Device has been disabled automatically and temporarily by system while application is running in the background.
}
public void EnableOrDisableDevice ( InputDevice device , bool enable , DeviceDisableScope scope = default )
{
if ( device = = null )
throw new ArgumentNullException ( nameof ( device ) ) ;
// Synchronize the enable/disabled state of the device.
if ( enable )
{
////REVIEW: Do we really want to allow overriding disabledWhileInBackground like it currently does?
// Enable device.
switch ( scope )
{
case DeviceDisableScope . Everywhere :
device . disabledWhileInBackground = false ;
if ( ! device . disabledInFrontend & & ! device . disabledInRuntime )
return ;
if ( device . disabledInRuntime )
{
device . ExecuteEnableCommand ( ) ;
device . disabledInRuntime = false ;
}
if ( device . disabledInFrontend )
{
if ( ! device . RequestSync ( ) )
ResetDevice ( device ) ;
device . disabledInFrontend = false ;
}
break ;
case DeviceDisableScope . InFrontendOnly :
device . disabledWhileInBackground = false ;
if ( ! device . disabledInFrontend & & device . disabledInRuntime )
return ;
if ( ! device . disabledInRuntime )
{
device . ExecuteDisableCommand ( ) ;
device . disabledInRuntime = true ;
}
if ( device . disabledInFrontend )
{
if ( ! device . RequestSync ( ) )
ResetDevice ( device ) ;
device . disabledInFrontend = false ;
}
break ;
case DeviceDisableScope . TemporaryWhilePlayerIsInBackground :
if ( device . disabledWhileInBackground )
{
if ( device . disabledInRuntime )
{
device . ExecuteEnableCommand ( ) ;
device . disabledInRuntime = false ;
}
if ( ! device . RequestSync ( ) )
ResetDevice ( device ) ;
device . disabledWhileInBackground = false ;
}
break ;
}
}
else
{
// Disable device.
switch ( scope )
{
case DeviceDisableScope . Everywhere :
device . disabledWhileInBackground = false ;
if ( device . disabledInFrontend & & device . disabledInRuntime )
return ;
if ( ! device . disabledInRuntime )
{
device . ExecuteDisableCommand ( ) ;
device . disabledInRuntime = true ;
}
if ( ! device . disabledInFrontend )
{
// When disabling a device, also issuing a reset in the backend is pointless.
ResetDevice ( device , issueResetCommand : false ) ;
device . disabledInFrontend = true ;
}
break ;
case DeviceDisableScope . InFrontendOnly :
device . disabledWhileInBackground = false ;
if ( ! device . disabledInRuntime & & device . disabledInFrontend )
return ;
if ( device . disabledInRuntime )
{
device . ExecuteEnableCommand ( ) ;
device . disabledInRuntime = false ;
}
if ( ! device . disabledInFrontend )
{
// When disabling a device, also issuing a reset in the backend is pointless.
ResetDevice ( device , issueResetCommand : false ) ;
device . disabledInFrontend = true ;
}
break ;
case DeviceDisableScope . TemporaryWhilePlayerIsInBackground :
// Won't flag a device as DisabledWhileInBackground if it is explicitly disabled in
// the frontend.
if ( device . disabledInFrontend | | device . disabledWhileInBackground )
return ;
device . disabledWhileInBackground = true ;
ResetDevice ( device , issueResetCommand : false ) ;
#if UNITY_EDITOR
if ( m_Settings . editorInputBehaviorInPlayMode = = InputSettings . EditorInputBehaviorInPlayMode . AllDeviceInputAlwaysGoesToGameView )
#endif
{
device . ExecuteDisableCommand ( ) ;
device . disabledInRuntime = true ;
}
break ;
}
}
// Let listeners know.
var deviceChange = enable ? InputDeviceChange . Enabled : InputDeviceChange . Disabled ;
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceChangeListeners , device , deviceChange , "InputSystem.onDeviceChange" ) ;
}
private unsafe void QueueEvent ( InputEvent * eventPtr )
{
// If we're currently in OnUpdate(), the m_InputEventStream will be open. In that case,
// append events directly to that buffer and do *NOT* go into native.
if ( m_InputEventStream . isOpen )
{
m_InputEventStream . Write ( eventPtr ) ;
return ;
}
// Don't bother keeping the data on the managed side. Just stuff the raw data directly
// into the native buffers. This also means this method is thread-safe.
m_Runtime . QueueEvent ( eventPtr ) ;
}
public unsafe void QueueEvent ( InputEventPtr ptr )
{
QueueEvent ( ptr . data ) ;
}
public unsafe void QueueEvent < TEvent > ( ref TEvent inputEvent )
where TEvent : struct , IInputEventTypeInfo
{
QueueEvent ( ( InputEvent * ) UnsafeUtility . AddressOf ( ref inputEvent ) ) ;
}
public void Update ( )
{
Update ( defaultUpdateType ) ;
}
public void Update ( InputUpdateType updateType )
{
m_Runtime . Update ( updateType ) ;
}
internal void Initialize ( IInputRuntime runtime , InputSettings settings )
{
Debug . Assert ( settings ! = null ) ;
m_Settings = settings ;
InitializeData ( ) ;
InstallRuntime ( runtime ) ;
InstallGlobals ( ) ;
ApplySettings ( ) ;
}
internal void Destroy ( )
{
// There isn't really much of a point in removing devices but we still
// want to clear out any global state they may be keeping. So just tell
// the devices that they got removed without actually removing them.
for ( var i = 0 ; i < m_DevicesCount ; + + i )
m_Devices [ i ] . NotifyRemoved ( ) ;
// Free all state memory.
m_StateBuffers . FreeAll ( ) ;
// Uninstall globals.
UninstallGlobals ( ) ;
// Destroy settings if they are temporary.
if ( m_Settings ! = null & & m_Settings . hideFlags = = HideFlags . HideAndDontSave )
Object . DestroyImmediate ( m_Settings ) ;
}
internal void InitializeData ( )
{
m_Layouts . Allocate ( ) ;
m_Processors . Initialize ( ) ;
m_Interactions . Initialize ( ) ;
m_Composites . Initialize ( ) ;
m_DevicesById = new Dictionary < int , InputDevice > ( ) ;
// Determine our default set of enabled update types. By
// default we enable both fixed and dynamic update because
// we don't know which one the user is going to use. The user
// can manually turn off one of them to optimize operation.
m_UpdateMask = InputUpdateType . Dynamic | InputUpdateType . Fixed ;
m_HasFocus = Application . isFocused ;
#if UNITY_EDITOR
m_EditorIsActive = true ;
m_UpdateMask | = InputUpdateType . Editor ;
#endif
// Default polling frequency is 60 Hz.
m_PollingFrequency = 60 ;
// Register layouts.
// NOTE: Base layouts must be registered before their derived layouts
// for the detection of base layouts to work.
RegisterControlLayout ( "Axis" , typeof ( AxisControl ) ) ; // Controls.
RegisterControlLayout ( "Button" , typeof ( ButtonControl ) ) ;
RegisterControlLayout ( "DiscreteButton" , typeof ( DiscreteButtonControl ) ) ;
RegisterControlLayout ( "Key" , typeof ( KeyControl ) ) ;
RegisterControlLayout ( "Analog" , typeof ( AxisControl ) ) ;
RegisterControlLayout ( "Integer" , typeof ( IntegerControl ) ) ;
RegisterControlLayout ( "Digital" , typeof ( IntegerControl ) ) ;
RegisterControlLayout ( "Double" , typeof ( DoubleControl ) ) ;
RegisterControlLayout ( "Vector2" , typeof ( Vector2Control ) ) ;
RegisterControlLayout ( "Vector3" , typeof ( Vector3Control ) ) ;
RegisterControlLayout ( "Delta" , typeof ( DeltaControl ) ) ;
RegisterControlLayout ( "Quaternion" , typeof ( QuaternionControl ) ) ;
RegisterControlLayout ( "Stick" , typeof ( StickControl ) ) ;
RegisterControlLayout ( "Dpad" , typeof ( DpadControl ) ) ;
RegisterControlLayout ( "DpadAxis" , typeof ( DpadControl . DpadAxisControl ) ) ;
RegisterControlLayout ( "AnyKey" , typeof ( AnyKeyControl ) ) ;
RegisterControlLayout ( "Touch" , typeof ( TouchControl ) ) ;
RegisterControlLayout ( "TouchPhase" , typeof ( TouchPhaseControl ) ) ;
RegisterControlLayout ( "TouchPress" , typeof ( TouchPressControl ) ) ;
RegisterControlLayout ( "Gamepad" , typeof ( Gamepad ) ) ; // Devices.
RegisterControlLayout ( "Joystick" , typeof ( Joystick ) ) ;
RegisterControlLayout ( "Keyboard" , typeof ( Keyboard ) ) ;
RegisterControlLayout ( "Pointer" , typeof ( Pointer ) ) ;
RegisterControlLayout ( "Mouse" , typeof ( Mouse ) ) ;
RegisterControlLayout ( "Pen" , typeof ( Pen ) ) ;
RegisterControlLayout ( "Touchscreen" , typeof ( Touchscreen ) ) ;
RegisterControlLayout ( "Sensor" , typeof ( Sensor ) ) ;
RegisterControlLayout ( "Accelerometer" , typeof ( Accelerometer ) ) ;
RegisterControlLayout ( "Gyroscope" , typeof ( Gyroscope ) ) ;
RegisterControlLayout ( "GravitySensor" , typeof ( GravitySensor ) ) ;
RegisterControlLayout ( "AttitudeSensor" , typeof ( AttitudeSensor ) ) ;
RegisterControlLayout ( "LinearAccelerationSensor" , typeof ( LinearAccelerationSensor ) ) ;
RegisterControlLayout ( "MagneticFieldSensor" , typeof ( MagneticFieldSensor ) ) ;
RegisterControlLayout ( "LightSensor" , typeof ( LightSensor ) ) ;
RegisterControlLayout ( "PressureSensor" , typeof ( PressureSensor ) ) ;
RegisterControlLayout ( "HumiditySensor" , typeof ( HumiditySensor ) ) ;
RegisterControlLayout ( "AmbientTemperatureSensor" , typeof ( AmbientTemperatureSensor ) ) ;
RegisterControlLayout ( "StepCounter" , typeof ( StepCounter ) ) ;
RegisterControlLayout ( "TrackedDevice" , typeof ( TrackedDevice ) ) ;
// Precompiled layouts.
RegisterPrecompiledLayout < FastKeyboard > ( FastKeyboard . metadata ) ;
RegisterPrecompiledLayout < FastTouchscreen > ( FastTouchscreen . metadata ) ;
RegisterPrecompiledLayout < FastMouse > ( FastMouse . metadata ) ;
// Register processors.
processors . AddTypeRegistration ( "Invert" , typeof ( InvertProcessor ) ) ;
processors . AddTypeRegistration ( "InvertVector2" , typeof ( InvertVector2Processor ) ) ;
processors . AddTypeRegistration ( "InvertVector3" , typeof ( InvertVector3Processor ) ) ;
processors . AddTypeRegistration ( "Clamp" , typeof ( ClampProcessor ) ) ;
processors . AddTypeRegistration ( "Normalize" , typeof ( NormalizeProcessor ) ) ;
processors . AddTypeRegistration ( "NormalizeVector2" , typeof ( NormalizeVector2Processor ) ) ;
processors . AddTypeRegistration ( "NormalizeVector3" , typeof ( NormalizeVector3Processor ) ) ;
processors . AddTypeRegistration ( "Scale" , typeof ( ScaleProcessor ) ) ;
processors . AddTypeRegistration ( "ScaleVector2" , typeof ( ScaleVector2Processor ) ) ;
processors . AddTypeRegistration ( "ScaleVector3" , typeof ( ScaleVector3Processor ) ) ;
processors . AddTypeRegistration ( "StickDeadzone" , typeof ( StickDeadzoneProcessor ) ) ;
processors . AddTypeRegistration ( "AxisDeadzone" , typeof ( AxisDeadzoneProcessor ) ) ;
processors . AddTypeRegistration ( "CompensateDirection" , typeof ( CompensateDirectionProcessor ) ) ;
processors . AddTypeRegistration ( "CompensateRotation" , typeof ( CompensateRotationProcessor ) ) ;
#if UNITY_EDITOR
processors . AddTypeRegistration ( "AutoWindowSpace" , typeof ( EditorWindowSpaceProcessor ) ) ;
#endif
// Register interactions.
interactions . AddTypeRegistration ( "Hold" , typeof ( HoldInteraction ) ) ;
interactions . AddTypeRegistration ( "Tap" , typeof ( TapInteraction ) ) ;
interactions . AddTypeRegistration ( "SlowTap" , typeof ( SlowTapInteraction ) ) ;
interactions . AddTypeRegistration ( "MultiTap" , typeof ( MultiTapInteraction ) ) ;
interactions . AddTypeRegistration ( "Press" , typeof ( PressInteraction ) ) ;
// Register composites.
composites . AddTypeRegistration ( "1DAxis" , typeof ( AxisComposite ) ) ;
composites . AddTypeRegistration ( "2DVector" , typeof ( Vector2Composite ) ) ;
composites . AddTypeRegistration ( "3DVector" , typeof ( Vector3Composite ) ) ;
composites . AddTypeRegistration ( "Axis" , typeof ( AxisComposite ) ) ; // Alias for pre-0.2 name.
composites . AddTypeRegistration ( "Dpad" , typeof ( Vector2Composite ) ) ; // Alias for pre-0.2 name.
composites . AddTypeRegistration ( "ButtonWithOneModifier" , typeof ( ButtonWithOneModifier ) ) ;
composites . AddTypeRegistration ( "ButtonWithTwoModifiers" , typeof ( ButtonWithTwoModifiers ) ) ;
composites . AddTypeRegistration ( "OneModifier" , typeof ( OneModifierComposite ) ) ;
composites . AddTypeRegistration ( "TwoModifiers" , typeof ( TwoModifiersComposite ) ) ;
}
internal void InstallRuntime ( IInputRuntime runtime )
{
if ( m_Runtime ! = null )
{
m_Runtime . onUpdate = null ;
m_Runtime . onBeforeUpdate = null ;
m_Runtime . onDeviceDiscovered = null ;
m_Runtime . onPlayerFocusChanged = null ;
m_Runtime . onShouldRunUpdate = null ;
#if UNITY_EDITOR
m_Runtime . onPlayerLoopInitialization = null ;
#endif
}
m_Runtime = runtime ;
m_Runtime . onUpdate = OnUpdate ;
m_Runtime . onDeviceDiscovered = OnNativeDeviceDiscovered ;
m_Runtime . onPlayerFocusChanged = OnFocusChanged ;
m_Runtime . onShouldRunUpdate = ShouldRunUpdate ;
#if UNITY_EDITOR
m_Runtime . onPlayerLoopInitialization = OnPlayerLoopInitialization ;
#endif
m_Runtime . pollingFrequency = pollingFrequency ;
m_HasFocus = m_Runtime . isPlayerFocused ;
// We only hook NativeInputSystem.onBeforeUpdate if necessary.
if ( m_BeforeUpdateListeners . length > 0 | | m_HaveDevicesWithStateCallbackReceivers )
{
m_Runtime . onBeforeUpdate = OnBeforeUpdate ;
m_NativeBeforeUpdateHooked = true ;
}
#if UNITY_ANALYTICS | | UNITY_EDITOR
InputAnalytics . Initialize ( this ) ;
m_Runtime . onShutdown = ( ) = > InputAnalytics . OnShutdown ( this ) ;
#endif
}
internal void InstallGlobals ( )
{
Debug . Assert ( m_Runtime ! = null ) ;
InputControlLayout . s_Layouts = m_Layouts ;
InputProcessor . s_Processors = m_Processors ;
InputInteraction . s_Interactions = m_Interactions ;
InputBindingComposite . s_Composites = m_Composites ;
InputRuntime . s_Instance = m_Runtime ;
InputRuntime . s_CurrentTimeOffsetToRealtimeSinceStartup =
m_Runtime . currentTimeOffsetToRealtimeSinceStartup ;
// Reset update state.
InputUpdate . Restore ( new InputUpdate . SerializedState ( ) ) ;
unsafe
{
InputStateBuffers . SwitchTo ( m_StateBuffers , InputUpdateType . Dynamic ) ;
InputStateBuffers . s_DefaultStateBuffer = m_StateBuffers . defaultStateBuffer ;
InputStateBuffers . s_NoiseMaskBuffer = m_StateBuffers . noiseMaskBuffer ;
InputStateBuffers . s_ResetMaskBuffer = m_StateBuffers . resetMaskBuffer ;
}
}
internal void UninstallGlobals ( )
{
if ( ReferenceEquals ( InputControlLayout . s_Layouts . baseLayoutTable , m_Layouts . baseLayoutTable ) )
InputControlLayout . s_Layouts = new InputControlLayout . Collection ( ) ;
if ( ReferenceEquals ( InputProcessor . s_Processors . table , m_Processors . table ) )
InputProcessor . s_Processors = new TypeTable ( ) ;
if ( ReferenceEquals ( InputInteraction . s_Interactions . table , m_Interactions . table ) )
InputInteraction . s_Interactions = new TypeTable ( ) ;
if ( ReferenceEquals ( InputBindingComposite . s_Composites . table , m_Composites . table ) )
InputBindingComposite . s_Composites = new TypeTable ( ) ;
// Clear layout cache.
InputControlLayout . s_CacheInstance = default ;
InputControlLayout . s_CacheInstanceRef = 0 ;
// Detach from runtime.
if ( m_Runtime ! = null )
{
m_Runtime . onUpdate = null ;
m_Runtime . onDeviceDiscovered = null ;
m_Runtime . onBeforeUpdate = null ;
m_Runtime . onPlayerFocusChanged = null ;
m_Runtime . onShouldRunUpdate = null ;
if ( ReferenceEquals ( InputRuntime . s_Instance , m_Runtime ) )
InputRuntime . s_Instance = null ;
}
}
[Serializable]
internal struct AvailableDevice
{
public InputDeviceDescription description ;
public int deviceId ;
public bool isNative ;
public bool isRemoved ;
}
// Used by EditorInputControlLayoutCache to determine whether its state is outdated.
internal int m_LayoutRegistrationVersion ;
private float m_PollingFrequency ;
internal InputControlLayout . Collection m_Layouts ;
private TypeTable m_Processors ;
private TypeTable m_Interactions ;
private TypeTable m_Composites ;
private int m_DevicesCount ;
private InputDevice [ ] m_Devices ;
private Dictionary < int , InputDevice > m_DevicesById ;
internal int m_AvailableDeviceCount ;
internal AvailableDevice [ ] m_AvailableDevices ; // A record of all devices reported to the system (from native or user code).
////REVIEW: should these be weak-referenced?
internal int m_DisconnectedDevicesCount ;
internal InputDevice [ ] m_DisconnectedDevices ;
internal InputUpdateType m_UpdateMask ; // Which of our update types are enabled.
private InputUpdateType m_CurrentUpdate ;
internal InputStateBuffers m_StateBuffers ;
#if UNITY_EDITOR
// remember time offset to correctly restore it after editor mode is done
private double latestNonEditorTimeOffsetToRealtimeSinceStartup ;
#endif
// We don't use UnityEvents and thus don't persist the callbacks during domain reloads.
// Restoration of UnityActions is unreliable and it's too easy to end up with double
// registrations what will lead to all kinds of misbehavior.
private CallbackArray < DeviceChangeListener > m_DeviceChangeListeners ;
private CallbackArray < DeviceStateChangeListener > m_DeviceStateChangeListeners ;
private CallbackArray < InputDeviceFindControlLayoutDelegate > m_DeviceFindLayoutCallbacks ;
internal CallbackArray < InputDeviceCommandDelegate > m_DeviceCommandCallbacks ;
private CallbackArray < LayoutChangeListener > m_LayoutChangeListeners ;
private CallbackArray < EventListener > m_EventListeners ;
private CallbackArray < UpdateListener > m_BeforeUpdateListeners ;
private CallbackArray < UpdateListener > m_AfterUpdateListeners ;
private CallbackArray < Action > m_SettingsChangedListeners ;
private bool m_NativeBeforeUpdateHooked ;
private bool m_HaveDevicesWithStateCallbackReceivers ;
private bool m_HasFocus ;
private InputEventStream m_InputEventStream ;
// We want to sync devices when the editor comes back into focus. Unfortunately, there's no
// callback for this so we have to poll this state.
#if UNITY_EDITOR
private bool m_EditorIsActive ;
#endif
// We allocate the 'executeDeviceCommand' closure passed to 'onFindLayoutForDevice'
// only once to avoid creating garbage.
private InputDeviceExecuteCommandDelegate m_DeviceFindExecuteCommandDelegate ;
private int m_DeviceFindExecuteCommandDeviceId ;
#if UNITY_ANALYTICS | | UNITY_EDITOR
private bool m_HaveSentStartupAnalytics ;
#endif
internal IInputRuntime m_Runtime ;
internal InputMetrics m_Metrics ;
internal InputSettings m_Settings ;
#if UNITY_EDITOR
internal IInputDiagnostics m_Diagnostics ;
#endif
////REVIEW: Make it so that device names *always* have a number appended? (i.e. Gamepad1, Gamepad2, etc. instead of Gamepad, Gamepad1, etc)
private void MakeDeviceNameUnique ( InputDevice device )
{
if ( m_DevicesCount = = 0 )
return ;
var deviceName = StringHelpers . MakeUniqueName ( device . name , m_Devices , x = > x ! = null ? x . name : string . Empty ) ;
if ( deviceName ! = device . name )
{
// If we have changed the name of the device, nuke all path strings in the control
// hierarchy so that they will get re-recreated when queried.
ResetControlPathsRecursive ( device ) ;
// Assign name.
device . m_Name = new InternedString ( deviceName ) ;
}
}
private static void ResetControlPathsRecursive ( InputControl control )
{
control . m_Path = null ;
var children = control . children ;
var childCount = children . Count ;
for ( var i = 0 ; i < childCount ; + + i )
ResetControlPathsRecursive ( children [ i ] ) ;
}
private void AssignUniqueDeviceId ( InputDevice device )
{
// If the device already has an ID, make sure it's unique.
if ( device . deviceId ! = InputDevice . InvalidDeviceId )
{
// Safety check to make sure out IDs are really unique.
// Given they are assigned by the native system they should be fine
// but let's make sure.
var existingDeviceWithId = TryGetDeviceById ( device . deviceId ) ;
if ( existingDeviceWithId ! = null )
throw new InvalidOperationException (
$"Duplicate device ID {device.deviceId} detected for devices '{device.name}' and '{existingDeviceWithId.name}'" ) ;
}
else
{
device . m_DeviceId = m_Runtime . AllocateDeviceId ( ) ;
}
}
// (Re)allocates state buffers and assigns each device that's been added
// a segment of the buffer. Preserves the current state of devices.
// NOTE: Installs the buffers globally.
private unsafe void ReallocateStateBuffers ( )
{
var oldBuffers = m_StateBuffers ;
// Allocate new buffers.
var newBuffers = new InputStateBuffers ( ) ;
newBuffers . AllocateAll ( m_Devices , m_DevicesCount ) ;
// Migrate state.
newBuffers . MigrateAll ( m_Devices , m_DevicesCount , oldBuffers ) ;
// Install the new buffers.
oldBuffers . FreeAll ( ) ;
m_StateBuffers = newBuffers ;
InputStateBuffers . s_DefaultStateBuffer = newBuffers . defaultStateBuffer ;
InputStateBuffers . s_NoiseMaskBuffer = newBuffers . noiseMaskBuffer ;
InputStateBuffers . s_ResetMaskBuffer = newBuffers . resetMaskBuffer ;
// Switch to buffers.
InputStateBuffers . SwitchTo ( m_StateBuffers ,
InputUpdate . s_LatestUpdateType ! = InputUpdateType . None ? InputUpdate . s_LatestUpdateType : defaultUpdateType ) ;
////TODO: need to update state change monitors
}
/// <summary>
/// Initialize default state for given device.
/// </summary>
/// <param name="device">A newly added input device.</param>
/// <remarks>
/// For every device, one copy of its state is kept around which is initialized with the default
/// values for the device. If the device has no control that has an explicitly specified control
/// value, the buffer simply contains all zeroes.
///
/// The default state buffer is initialized once when a device is added to the system and then
/// migrated by <see cref="InputStateBuffers"/> like other device state and removed when the device
/// is removed from the system.
/// </remarks>
private unsafe void InitializeDefaultState ( InputDevice device )
{
// Nothing to do if device has a default state of all zeroes.
if ( ! device . hasControlsWithDefaultState )
return ;
// Otherwise go through each control and write its default value.
var controls = device . allControls ;
var controlCount = controls . Count ;
var defaultStateBuffer = m_StateBuffers . defaultStateBuffer ;
for ( var n = 0 ; n < controlCount ; + + n )
{
var control = controls [ n ] ;
if ( ! control . hasDefaultState )
continue ;
control . m_StateBlock . Write ( defaultStateBuffer , control . m_DefaultState ) ;
}
// Copy default state to all front and back buffers.
var stateBlock = device . m_StateBlock ;
var deviceIndex = device . m_DeviceIndex ;
if ( m_StateBuffers . m_PlayerStateBuffers . valid )
{
stateBlock . CopyToFrom ( m_StateBuffers . m_PlayerStateBuffers . GetFrontBuffer ( deviceIndex ) , defaultStateBuffer ) ;
stateBlock . CopyToFrom ( m_StateBuffers . m_PlayerStateBuffers . GetBackBuffer ( deviceIndex ) , defaultStateBuffer ) ;
}
#if UNITY_EDITOR
if ( m_StateBuffers . m_EditorStateBuffers . valid )
{
stateBlock . CopyToFrom ( m_StateBuffers . m_EditorStateBuffers . GetFrontBuffer ( deviceIndex ) , defaultStateBuffer ) ;
stateBlock . CopyToFrom ( m_StateBuffers . m_EditorStateBuffers . GetBackBuffer ( deviceIndex ) , defaultStateBuffer ) ;
}
#endif
}
private unsafe void InitializeDeviceState ( InputDevice device )
{
Debug . Assert ( device ! = null , "Device must not be null" ) ;
Debug . Assert ( device . added , "Device must have been added" ) ;
Debug . Assert ( device . stateBlock . byteOffset ! = InputStateBlock . InvalidOffset , "Device state block offset is invalid" ) ;
Debug . Assert ( device . stateBlock . byteOffset + device . stateBlock . alignedSizeInBytes < = m_StateBuffers . sizePerBuffer ,
"Device state block is not contained in state buffer" ) ;
var controls = device . allControls ;
var controlCount = controls . Count ;
var resetMaskBuffer = m_StateBuffers . resetMaskBuffer ;
var haveControlsWithDefaultState = device . hasControlsWithDefaultState ;
// Assume that everything in the device is noise. This way we also catch memory regions
// that are not actually covered by a control and implicitly mark them as noise (e.g. the
// report ID in HID input reports).
//
// NOTE: Noise is indicated by *unset* bits so we don't have to do anything here to start
// with all-noise as we expect noise mask memory to be cleared on allocation.
var noiseMaskBuffer = m_StateBuffers . noiseMaskBuffer ;
// We first toggle all bits *on* and then toggle bits for noisy and dontReset controls *off* individually.
// We do this instead of just leaving all bits *off* and then going through controls that aren't noisy/dontReset *on*.
// If we did the latter, we'd have the problem that a parent control such as TouchControl would toggle on bits for
// the entirety of its state block and thus cover the state of all its child controls.
MemoryHelpers . SetBitsInBuffer ( noiseMaskBuffer , ( int ) device . stateBlock . byteOffset , 0 , ( int ) device . stateBlock . sizeInBits , false ) ;
MemoryHelpers . SetBitsInBuffer ( resetMaskBuffer , ( int ) device . stateBlock . byteOffset , 0 , ( int ) device . stateBlock . sizeInBits , true ) ;
// Go through controls.
var defaultStateBuffer = m_StateBuffers . defaultStateBuffer ;
for ( var n = 0 ; n < controlCount ; + + n )
{
var control = controls [ n ] ;
// Don't allow controls that hijack state from other controls to set independent noise or dontReset flags.
if ( control . usesStateFromOtherControl )
continue ;
if ( ! control . noisy | | control . dontReset )
{
ref var stateBlock = ref control . m_StateBlock ;
Debug . Assert ( stateBlock . byteOffset ! = InputStateBlock . InvalidOffset , "Byte offset is invalid on control's state block" ) ;
Debug . Assert ( stateBlock . bitOffset ! = InputStateBlock . InvalidOffset , "Bit offset is invalid on control's state block" ) ;
Debug . Assert ( stateBlock . sizeInBits ! = InputStateBlock . InvalidOffset , "Size is invalid on control's state block" ) ;
Debug . Assert ( stateBlock . byteOffset > = device . stateBlock . byteOffset , "Control's offset is located below device's offset" ) ;
Debug . Assert ( stateBlock . byteOffset + stateBlock . alignedSizeInBytes < =
device . stateBlock . byteOffset + device . stateBlock . alignedSizeInBytes , "Control state block lies outside of state buffer" ) ;
// If control isn't noisy, toggle its bits *on* in the noise mask.
if ( ! control . noisy )
MemoryHelpers . SetBitsInBuffer ( noiseMaskBuffer , ( int ) stateBlock . byteOffset , ( int ) stateBlock . bitOffset ,
( int ) stateBlock . sizeInBits , true ) ;
// If control shouldn't be reset, toggle its bits *off* in the reset mask.
if ( control . dontReset )
MemoryHelpers . SetBitsInBuffer ( resetMaskBuffer , ( int ) stateBlock . byteOffset , ( int ) stateBlock . bitOffset ,
( int ) stateBlock . sizeInBits , false ) ;
}
// If control has default state, write it into to the device's default state.
if ( haveControlsWithDefaultState & & control . hasDefaultState )
control . m_StateBlock . Write ( defaultStateBuffer , control . m_DefaultState ) ;
}
// Copy default state to all front and back buffers.
if ( haveControlsWithDefaultState )
{
ref var deviceStateBlock = ref device . m_StateBlock ;
var deviceIndex = device . m_DeviceIndex ;
if ( m_StateBuffers . m_PlayerStateBuffers . valid )
{
deviceStateBlock . CopyToFrom ( m_StateBuffers . m_PlayerStateBuffers . GetFrontBuffer ( deviceIndex ) , defaultStateBuffer ) ;
deviceStateBlock . CopyToFrom ( m_StateBuffers . m_PlayerStateBuffers . GetBackBuffer ( deviceIndex ) , defaultStateBuffer ) ;
}
#if UNITY_EDITOR
if ( m_StateBuffers . m_EditorStateBuffers . valid )
{
deviceStateBlock . CopyToFrom ( m_StateBuffers . m_EditorStateBuffers . GetFrontBuffer ( deviceIndex ) , defaultStateBuffer ) ;
deviceStateBlock . CopyToFrom ( m_StateBuffers . m_EditorStateBuffers . GetBackBuffer ( deviceIndex ) , defaultStateBuffer ) ;
}
#endif
}
}
private void OnNativeDeviceDiscovered ( int deviceId , string deviceDescriptor )
{
// Make sure we're not adding to m_AvailableDevices before we restored what we
// had before a domain reload.
RestoreDevicesAfterDomainReloadIfNecessary ( ) ;
// See if we have a disconnected device we can revive.
// NOTE: We do this all the way up here as the first thing before we even parse the JSON descriptor so
// if we do have a device we can revive, we can do so without allocating any GC memory.
var device = TryMatchDisconnectedDevice ( deviceDescriptor ) ;
// Parse description, if need be.
var description = device ? . description ? ? InputDeviceDescription . FromJson ( deviceDescriptor ) ;
// Add it.
var markAsRemoved = false ;
try
{
// If we have a restricted set of supported devices, first check if it's a device
// we should support.
if ( m_Settings . supportedDevices . Count > 0 )
{
var layout = device ! = null ? device . m_Layout : TryFindMatchingControlLayout ( ref description , deviceId ) ;
if ( ! IsDeviceLayoutMarkedAsSupportedInSettings ( layout ) )
{
// Not supported. Ignore device. Still will get added to m_AvailableDevices
// list in finally clause below. If later the set of supported devices changes
// so that the device is now supported, ApplySettings() will pull it back out
// and create the device.
markAsRemoved = true ;
return ;
}
}
if ( device ! = null )
{
// It's a device we pulled from the disconnected list. Update the device with the
// new ID, re-add it and notify that we've reconnected.
device . m_DeviceId = deviceId ;
device . m_DeviceFlags | = InputDevice . DeviceFlags . Native ;
device . m_DeviceFlags & = ~ InputDevice . DeviceFlags . DisabledInFrontend ;
device . m_DeviceFlags & = ~ InputDevice . DeviceFlags . DisabledWhileInBackground ;
device . m_DeviceFlags & = ~ InputDevice . DeviceFlags . DisabledStateHasBeenQueriedFromRuntime ;
AddDevice ( device ) ;
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceChangeListeners , device , InputDeviceChange . Reconnected ,
"InputSystem.onDeviceChange" ) ;
}
else
{
// Go through normal machinery to try to create a new device.
AddDevice ( description , throwIfNoLayoutFound : false , deviceId : deviceId ,
deviceFlags : InputDevice . DeviceFlags . Native ) ;
}
}
// We're catching exceptions very aggressively here. The reason is that we don't want
// exceptions thrown as a result of trying to create devices from device discoveries reported
// by native to break the system as a whole. Instead, we want to make the error visible but then
// go and work with whatever devices we *did* manage to create successfully.
catch ( Exception exception )
{
Debug . LogError ( $"Could not create a device for '{description}' (exception: {exception})" ) ;
}
finally
{
// Remember it. Do this *after* the AddDevice() call above so that if there's
// a listener creating layouts on the fly we won't end up matching this device and
// create an InputDevice right away (which would then conflict with the one we
// create in AddDevice).
ArrayHelpers . AppendWithCapacity ( ref m_AvailableDevices , ref m_AvailableDeviceCount ,
new AvailableDevice
{
description = description ,
deviceId = deviceId ,
isNative = true ,
isRemoved = markAsRemoved ,
} ) ;
}
}
private InputDevice TryMatchDisconnectedDevice ( string deviceDescriptor )
{
for ( var i = 0 ; i < m_DisconnectedDevicesCount ; + + i )
{
var device = m_DisconnectedDevices [ i ] ;
var description = device . description ;
// We don't parse the full description but rather go property by property in order to not
// allocate GC memory if we can avoid it.
if ( ! InputDeviceDescription . ComparePropertyToDeviceDescriptor ( "interface" , description . interfaceName , deviceDescriptor ) )
continue ;
if ( ! InputDeviceDescription . ComparePropertyToDeviceDescriptor ( "product" , description . product , deviceDescriptor ) )
continue ;
if ( ! InputDeviceDescription . ComparePropertyToDeviceDescriptor ( "manufacturer" , description . manufacturer , deviceDescriptor ) )
continue ;
if ( ! InputDeviceDescription . ComparePropertyToDeviceDescriptor ( "type" , description . deviceClass , deviceDescriptor ) )
continue ;
if ( ! InputDeviceDescription . ComparePropertyToDeviceDescriptor ( "capabilities" , description . capabilities , deviceDescriptor ) )
continue ;
if ( ! InputDeviceDescription . ComparePropertyToDeviceDescriptor ( "serial" , description . serial , deviceDescriptor ) )
continue ;
ArrayHelpers . EraseAtWithCapacity ( m_DisconnectedDevices , ref m_DisconnectedDevicesCount , i ) ;
return device ;
}
return null ;
}
private void InstallBeforeUpdateHookIfNecessary ( )
{
if ( m_NativeBeforeUpdateHooked | | m_Runtime = = null )
return ;
m_Runtime . onBeforeUpdate = OnBeforeUpdate ;
m_NativeBeforeUpdateHooked = true ;
}
private void RestoreDevicesAfterDomainReloadIfNecessary ( )
{
#if UNITY_EDITOR
if ( m_SavedDeviceStates ! = null )
RestoreDevicesAfterDomainReload ( ) ;
#endif
}
#if UNITY_EDITOR
private void SyncAllDevicesWhenEditorIsActivated ( )
{
var isActive = m_Runtime . isEditorActive ;
if ( isActive = = m_EditorIsActive )
return ;
m_EditorIsActive = isActive ;
if ( m_EditorIsActive )
SyncAllDevices ( ) ;
}
private void SyncAllDevices ( )
{
for ( var i = 0 ; i < m_DevicesCount ; + + i )
{
// When the editor comes back into focus, we actually do want resets to happen
// for devices that don't support syncs as they will likely have missed input while
// we were in the background.
if ( ! m_Devices [ i ] . RequestSync ( ) )
ResetDevice ( m_Devices [ i ] , issueResetCommand : true ) ;
}
}
internal void SyncAllDevicesAfterEnteringPlayMode ( )
{
// Because we ignore all events between exiting edit mode and entering play mode,
// that includes any potential device resets/syncs/etc,
// we need to resync all devices after we're in play mode proper.
////TODO: this is a hacky workaround, implement a proper solution where events from sync/resets are not ignored.
SyncAllDevices ( ) ;
}
#endif
private void WarnAboutDevicesFailingToRecreateAfterDomainReload ( )
{
// If we still have any saved device states, we have devices that we couldn't figure
// out how to recreate after a domain reload. Log a warning for each of them and
// let go of them.
#if UNITY_EDITOR
if ( m_SavedDeviceStates = = null )
return ;
for ( var i = 0 ; i < m_SavedDeviceStates . Length ; + + i )
{
ref var state = ref m_SavedDeviceStates [ i ] ;
Debug . LogWarning ( $"Could not recreate device '{state.name}' with layout '{state.layout}' after domain reload" ) ;
}
// At this point, we throw the device states away and forget about
// what we had before the domain reload.
m_SavedDeviceStates = null ;
#endif
}
private void OnBeforeUpdate ( InputUpdateType updateType )
{
// Restore devices before checking update mask. See InputSystem.RunInitialUpdate().
RestoreDevicesAfterDomainReloadIfNecessary ( ) ;
if ( ( updateType & m_UpdateMask ) = = 0 )
return ;
InputStateBuffers . SwitchTo ( m_StateBuffers , updateType ) ;
InputUpdate . OnBeforeUpdate ( updateType ) ;
// For devices that have state callbacks, tell them we're carrying state over
// into the next frame.
if ( m_HaveDevicesWithStateCallbackReceivers & & updateType ! = InputUpdateType . BeforeRender ) ////REVIEW: before-render handling is probably wrong
{
for ( var i = 0 ; i < m_DevicesCount ; + + i )
{
var device = m_Devices [ i ] ;
if ( ! device . hasStateCallbacks )
continue ;
// NOTE: We do *not* perform a buffer flip here as we do not want to change what is the
// current and what is the previous state when we carry state forward. Rather,
// OnBeforeUpdate, if it modifies state, it modifies the current state directly.
// Also, for the same reasons, we do not modify the dynamic/fixed update counts
// on the device. If an event comes in in the upcoming update, it should lead to
// a buffer flip.
( ( IInputStateCallbackReceiver ) device ) . OnNextUpdate ( ) ;
}
}
DelegateHelpers . InvokeCallbacksSafe ( ref m_BeforeUpdateListeners , "onBeforeUpdate" ) ;
}
/// <summary>
/// Apply the settings in <see cref="m_Settings"/>.
/// </summary>
internal void ApplySettings ( )
{
// Sync update mask.
var newUpdateMask = InputUpdateType . Editor ;
if ( ( m_UpdateMask & InputUpdateType . BeforeRender ) ! = 0 )
{
// BeforeRender updates are enabled in response to devices needing BeforeRender updates
// so we always preserve this if set.
newUpdateMask | = InputUpdateType . BeforeRender ;
}
if ( m_Settings . updateMode = = InputSettings . s_OldUnsupportedFixedAndDynamicUpdateSetting )
m_Settings . updateMode = InputSettings . UpdateMode . ProcessEventsInDynamicUpdate ;
switch ( m_Settings . updateMode )
{
case InputSettings . UpdateMode . ProcessEventsInDynamicUpdate :
newUpdateMask | = InputUpdateType . Dynamic ;
break ;
case InputSettings . UpdateMode . ProcessEventsInFixedUpdate :
newUpdateMask | = InputUpdateType . Fixed ;
break ;
case InputSettings . UpdateMode . ProcessEventsManually :
newUpdateMask | = InputUpdateType . Manual ;
break ;
default :
throw new NotSupportedException ( "Invalid input update mode: " + m_Settings . updateMode ) ;
}
#if UNITY_EDITOR
// In the editor, we force editor updates to be on even if InputEditorUserSettings.lockInputToGameView is
// on as otherwise we'll end up accumulating events in edit mode without anyone flushing the
// queue out regularly.
newUpdateMask | = InputUpdateType . Editor ;
#endif
updateMask = newUpdateMask ;
////TODO: optimize this so that we don't repeatedly recreate state if we add/remove multiple devices
//// (same goes for not resolving actions repeatedly)
// Check if there's any native device we aren't using ATM which now fits
// the set of supported devices.
AddAvailableDevicesThatAreNowRecognized ( ) ;
// If the settings restrict the set of supported devices, demote any native
// device we currently have that doesn't fit the requirements.
if ( settings . supportedDevices . Count > 0 )
{
for ( var i = 0 ; i < m_DevicesCount ; + + i )
{
var device = m_Devices [ i ] ;
var layout = device . m_Layout ;
// If it's not in m_AvailableDevices, we don't automatically remove it.
// Whatever has been added directly through AddDevice(), we keep and don't
// restrict by `supportDevices`.
var isInAvailableDevices = false ;
for ( var n = 0 ; n < m_AvailableDeviceCount ; + + n )
{
if ( m_AvailableDevices [ n ] . deviceId = = device . deviceId )
{
isInAvailableDevices = true ;
break ;
}
}
if ( ! isInAvailableDevices )
continue ;
// If the device layout isn't supported according to the current settings,
// remove the device.
if ( ! IsDeviceLayoutMarkedAsSupportedInSettings ( layout ) )
{
RemoveDevice ( device , keepOnListOfAvailableDevices : true ) ;
- - i ;
}
}
}
// Apply feature flags.
if ( m_Settings . m_FeatureFlags ! = null )
{
#if UNITY_EDITOR
runPlayerUpdatesInEditMode = m_Settings . IsFeatureEnabled ( InputFeatureNames . kRunPlayerUpdatesInEditMode ) ;
#endif
if ( m_Settings . IsFeatureEnabled ( InputFeatureNames . kUseWindowsGamingInputBackend ) )
{
var command = UseWindowsGamingInputCommand . Create ( true ) ;
if ( ExecuteGlobalCommand ( ref command ) < 0 )
Debug . LogError ( $"Could not enable Windows.Gaming.Input" ) ;
}
}
// Cache some values.
Touchscreen . s_TapTime = settings . defaultTapTime ;
Touchscreen . s_TapDelayTime = settings . multiTapDelayTime ;
Touchscreen . s_TapRadiusSquared = settings . tapRadius * settings . tapRadius ;
// Extra clamp here as we can't tell what we're getting from serialized data.
ButtonControl . s_GlobalDefaultButtonPressPoint = Mathf . Clamp ( settings . defaultButtonPressPoint , ButtonControl . kMinButtonPressPoint , float . MaxValue ) ;
ButtonControl . s_GlobalDefaultButtonReleaseThreshold = settings . buttonReleaseThreshold ;
2023-05-07 18:43:11 -04:00
// Update devices control optimization
foreach ( var device in devices )
device . SetOptimizedControlDataTypeRecursively ( ) ;
// Invalidate control caches due to potential changes to processors or value readers
foreach ( var device in devices )
device . MarkAsStaleRecursively ( ) ;
2023-03-28 13:24:16 -04:00
// Let listeners know.
DelegateHelpers . InvokeCallbacksSafe ( ref m_SettingsChangedListeners ,
"InputSystem.onSettingsChange" ) ;
}
internal unsafe long ExecuteGlobalCommand < TCommand > ( ref TCommand command )
where TCommand : struct , IInputDeviceCommandInfo
{
var ptr = ( InputDeviceCommand * ) UnsafeUtility . AddressOf ( ref command ) ;
// device id is irrelevant as we route it based on fourcc internally
return InputRuntime . s_Instance . DeviceCommand ( 0 , ptr ) ;
}
internal void AddAvailableDevicesThatAreNowRecognized ( )
{
for ( var i = 0 ; i < m_AvailableDeviceCount ; + + i )
{
var id = m_AvailableDevices [ i ] . deviceId ;
if ( TryGetDeviceById ( id ) ! = null )
continue ;
var layout = TryFindMatchingControlLayout ( ref m_AvailableDevices [ i ] . description , id ) ;
if ( ! IsDeviceLayoutMarkedAsSupportedInSettings ( layout ) ) continue ;
if ( layout . IsEmpty ( ) )
{
// If it's a device coming from the runtime, disable it.
if ( id ! = InputDevice . InvalidDeviceId )
{
var command = DisableDeviceCommand . Create ( ) ;
m_Runtime . DeviceCommand ( id , ref command ) ;
}
continue ;
}
try
{
AddDevice ( m_AvailableDevices [ i ] . description , layout , deviceId : id ,
deviceFlags : m_AvailableDevices [ i ] . isNative ? InputDevice . DeviceFlags . Native : 0 ) ;
}
catch ( Exception )
{
// the user might have changed the layout of one device, but others in the system might still have
// layouts we can't make sense of. Just quietly swallow exceptions from those so as not to spam
// the user with information about devices unrelated to what was actually changed.
}
}
}
private bool ShouldRunDeviceInBackground ( InputDevice device )
{
var runDeviceInBackground =
m_Settings . backgroundBehavior ! = InputSettings . BackgroundBehavior . ResetAndDisableAllDevices & &
device . canRunInBackground ;
// In editor, we may override canRunInBackground depending on the gameViewFocus setting.
#if UNITY_EDITOR
if ( runDeviceInBackground )
{
if ( m_Settings . editorInputBehaviorInPlayMode = = InputSettings . EditorInputBehaviorInPlayMode . AllDevicesRespectGameViewFocus )
runDeviceInBackground = false ;
else if ( m_Settings . editorInputBehaviorInPlayMode = = InputSettings . EditorInputBehaviorInPlayMode . PointersAndKeyboardsRespectGameViewFocus )
runDeviceInBackground = ! ( device is Pointer | | device is Keyboard ) ;
}
#endif
return runDeviceInBackground ;
}
internal void OnFocusChanged ( bool focus )
{
#if UNITY_EDITOR
SyncAllDevicesWhenEditorIsActivated ( ) ;
if ( ! m_Runtime . isInPlayMode )
{
m_HasFocus = focus ;
return ;
}
#endif
#if UNITY_EDITOR
var gameViewFocus = m_Settings . editorInputBehaviorInPlayMode ;
#endif
var runInBackground =
#if UNITY_EDITOR
// In the editor, the player loop will always be run even if the Game View does not have focus. This
// amounts to runInBackground being always true in the editor, regardless of what the setting in
// the Player Settings window is.
//
// If, however, "Game View Focus" is set to "Exactly As In Player", we force code here down the same
// path as in the player.
gameViewFocus ! = InputSettings . EditorInputBehaviorInPlayMode . AllDeviceInputAlwaysGoesToGameView | | m_Runtime . runInBackground ;
#else
m_Runtime . runInBackground ;
#endif
var backgroundBehavior = m_Settings . backgroundBehavior ;
if ( backgroundBehavior = = InputSettings . BackgroundBehavior . IgnoreFocus & & runInBackground )
{
// If runInBackground is true, no device changes should happen, even when focus is gained. So early out.
// If runInBackground is false, we still want to sync devices when focus is gained. So we need to continue further.
m_HasFocus = focus ;
return ;
}
#if UNITY_EDITOR
// Set the current update type while we process the focus changes to make sure we
// feed into the right buffer. No need to do this in the player as it doesn't have
// the editor/player confusion.
m_CurrentUpdate = m_UpdateMask . GetUpdateTypeForPlayer ( ) ;
#endif
if ( ! focus )
{
// We only react to loss of focus when we will keep running in the background. If not,
// we'll do nothing and just wait for focus to come back (where we then try to sync all devices).
if ( runInBackground )
{
for ( var i = 0 ; i < m_DevicesCount ; + + i )
{
// Determine whether to run this device in the background.
var device = m_Devices [ i ] ;
if ( ! device . enabled | | ShouldRunDeviceInBackground ( device ) )
continue ;
// Disable the device. This will also soft-reset it.
EnableOrDisableDevice ( device , false , DeviceDisableScope . TemporaryWhilePlayerIsInBackground ) ;
// In case we invoked a callback that messed with our device array, adjust our index.
var index = m_Devices . IndexOfReference ( device , m_DevicesCount ) ;
if ( index = = - 1 )
- - i ;
else
i = index ;
}
}
}
else
{
// On focus gain, reenable and sync devices.
for ( var i = 0 ; i < m_DevicesCount ; + + i )
{
var device = m_Devices [ i ] ;
// Re-enable the device if we disabled it on focus loss. This will also issue a sync.
if ( device . disabledWhileInBackground )
EnableOrDisableDevice ( device , true , DeviceDisableScope . TemporaryWhilePlayerIsInBackground ) ;
// Try to sync. If it fails and we didn't run in the background, perform
// a reset instead. This is to cope with backends that are unable to sync but
// may still retain state which now may be outdated because the input device may
// have changed state while we weren't running. So at least make the backend flush
// its state (if any).
else if ( device . enabled & & ! runInBackground & & ! device . RequestSync ( ) )
ResetDevice ( device ) ;
}
}
#if UNITY_EDITOR
m_CurrentUpdate = InputUpdateType . None ;
#endif
// We set this *after* the block above as defaultUpdateType is influenced by the setting.
m_HasFocus = focus ;
}
#if UNITY_EDITOR
internal void LeavePlayMode ( )
{
// Reenable all devices and reset their play mode state.
m_CurrentUpdate = InputUpdate . GetUpdateTypeForPlayer ( m_UpdateMask ) ;
InputStateBuffers . SwitchTo ( m_StateBuffers , m_CurrentUpdate ) ;
for ( var i = 0 ; i < m_DevicesCount ; + + i )
{
var device = m_Devices [ i ] ;
if ( device . disabledWhileInBackground )
EnableOrDisableDevice ( device , true , scope : DeviceDisableScope . TemporaryWhilePlayerIsInBackground ) ;
ResetDevice ( device , alsoResetDontResetControls : true ) ;
}
m_CurrentUpdate = default ;
}
private void OnPlayerLoopInitialization ( )
{
if ( ! gameIsPlaying | | // if game is not playing
! InputUpdate . s_LatestUpdateType . IsEditorUpdate ( ) | | // or last update was not editor update
! InputUpdate . s_LatestNonEditorUpdateType . IsPlayerUpdate ( ) ) // or update before that was not player update
return ; // then no need to restore anything
InputUpdate . RestoreStateAfterEditorUpdate ( ) ;
InputRuntime . s_CurrentTimeOffsetToRealtimeSinceStartup = latestNonEditorTimeOffsetToRealtimeSinceStartup ;
InputStateBuffers . SwitchTo ( m_StateBuffers , InputUpdate . s_LatestUpdateType ) ;
}
#endif
internal bool ShouldRunUpdate ( InputUpdateType updateType )
{
// We perform a "null" update after domain reloads and on startup to get our devices
// in place before the runtime calls MonoBehaviour callbacks. See InputSystem.RunInitialUpdate().
if ( updateType = = InputUpdateType . None )
return true ;
var mask = m_UpdateMask ;
#if UNITY_EDITOR
// If the player isn't running, the only thing we run is editor updates, except if
// explicitly overriden via `runUpdatesInEditMode`.
// NOTE: This means that in edit mode (outside of play mode) we *never* switch to player
// input state. So, any script anywhere will see input state from the editor. If you
// have an [ExecuteInEditMode] MonoBehaviour and it polls the gamepad, for example,
// it will see gamepad inputs going to the editor and respond to them.
if ( ! gameIsPlaying & & updateType ! = InputUpdateType . Editor & & ! runPlayerUpdatesInEditMode )
return false ;
#endif
return ( updateType & mask ) ! = 0 ;
}
/// <summary>
/// Process input events.
/// </summary>
/// <param name="updateType"></param>
/// <param name="eventBuffer"></param>
/// <remarks>
/// This method is the core workhorse of the input system. It is called from <see cref="UnityEngineInternal.Input.NativeInputSystem"/>.
/// Usually this happens in response to the player loop running and triggering updates at set points. However,
/// updates can also be manually triggered through <see cref="InputSystem.Update"/>.
///
/// The method receives the event buffer used internally by the runtime to collect events.
///
/// Note that update types do *NOT* say what the events we receive are for. The update type only indicates
/// where in the Unity's application loop we got called from. Where the event data goes depends wholly on
/// which buffers we activate in the update and write the event data into.
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown if OnUpdate is called recursively.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")]
private unsafe void OnUpdate ( InputUpdateType updateType , ref InputEventBuffer eventBuffer )
{
////TODO: switch from Profiler to CustomSampler API
// NOTE: This is *not* using try/finally as we've seen unreliability in the EndSample()
// execution (and we're not sure where it's coming from).
Profiler . BeginSample ( "InputUpdate" ) ;
if ( m_InputEventStream . isOpen )
throw new InvalidOperationException ( "Already have an event buffer set! Was OnUpdate() called recursively?" ) ;
// Restore devices before checking update mask. See InputSystem.RunInitialUpdate().
RestoreDevicesAfterDomainReloadIfNecessary ( ) ;
// In the editor, we issue a sync on all devices when the editor comes back to the foreground.
#if UNITY_EDITOR
SyncAllDevicesWhenEditorIsActivated ( ) ;
#endif
if ( ( updateType & m_UpdateMask ) = = 0 )
{
Profiler . EndSample ( ) ;
return ;
}
WarnAboutDevicesFailingToRecreateAfterDomainReload ( ) ;
// First update sends out startup analytics.
#if UNITY_ANALYTICS | | UNITY_EDITOR
if ( ! m_HaveSentStartupAnalytics )
{
InputAnalytics . OnStartup ( this ) ;
m_HaveSentStartupAnalytics = true ;
}
#endif
// Update metrics.
+ + m_Metrics . totalUpdateCount ;
#if UNITY_EDITOR
// If current update is editor update and previous update was non-editor,
// store the time offset so we can restore it right after editor update is complete
if ( ( ( updateType & InputUpdateType . Editor ) = = InputUpdateType . Editor ) & & ( m_CurrentUpdate & InputUpdateType . Editor ) = = 0 )
latestNonEditorTimeOffsetToRealtimeSinceStartup =
InputRuntime . s_CurrentTimeOffsetToRealtimeSinceStartup ;
#endif
// Store current time offset.
InputRuntime . s_CurrentTimeOffsetToRealtimeSinceStartup = m_Runtime . currentTimeOffsetToRealtimeSinceStartup ;
InputStateBuffers . SwitchTo ( m_StateBuffers , updateType ) ;
m_CurrentUpdate = updateType ;
InputUpdate . OnUpdate ( updateType ) ;
2023-05-07 18:43:11 -04:00
// Ensure optimized controls are in valid state
foreach ( var device in devices )
device . EnsureOptimizationTypeHasNotChanged ( ) ;
2023-03-28 13:24:16 -04:00
var shouldProcessActionTimeouts = updateType . IsPlayerUpdate ( ) & & gameIsPlaying ;
// See if we're supposed to only take events up to a certain time.
// NOTE: We do not require the events in the queue to be sorted. Instead, we will walk over
// all events in the buffer each time. Note that if there are multiple events for the same
// device, it depends on the producer of these events to queue them in correct order.
// Otherwise, once an event with a newer timestamp has been processed, events coming later
// in the buffer and having older timestamps will get rejected.
var currentTime = updateType = = InputUpdateType . Fixed ? m_Runtime . currentTimeForFixedUpdate : m_Runtime . currentTime ;
var timesliceEvents = ( updateType = = InputUpdateType . Fixed | | updateType = = InputUpdateType . BeforeRender ) & &
InputSystem . settings . updateMode = = InputSettings . UpdateMode . ProcessEventsInFixedUpdate ;
// Figure out if we can just flush the buffer and early out.
var canFlushBuffer =
false
#if UNITY_EDITOR
// If out of focus and runInBackground is off and ExactlyAsInPlayer is on, discard input.
| | ( ! gameHasFocus & & m_Settings . editorInputBehaviorInPlayMode = = InputSettings . EditorInputBehaviorInPlayMode . AllDeviceInputAlwaysGoesToGameView & &
( ! m_Runtime . runInBackground | |
m_Settings . backgroundBehavior = = InputSettings . BackgroundBehavior . ResetAndDisableAllDevices ) )
#else
| | ( ! gameHasFocus & & ! m_Runtime . runInBackground )
#endif
;
var canEarlyOut =
// Early out if there's no events to process.
eventBuffer . eventCount = = 0
| | canFlushBuffer | |
// If we're in the background and not supposed to process events in this update (but somehow
// still ended up here), we're done.
( ( ! gameHasFocus | | gameShouldGetInputRegardlessOfFocus ) & &
( ( m_Settings . backgroundBehavior = = InputSettings . BackgroundBehavior . ResetAndDisableAllDevices & & updateType ! = InputUpdateType . Editor )
#if UNITY_EDITOR
| | ( m_Settings . editorInputBehaviorInPlayMode = = InputSettings . EditorInputBehaviorInPlayMode . AllDevicesRespectGameViewFocus & & updateType ! = InputUpdateType . Editor )
| | ( m_Settings . backgroundBehavior = = InputSettings . BackgroundBehavior . IgnoreFocus & & m_Settings . editorInputBehaviorInPlayMode = = InputSettings . EditorInputBehaviorInPlayMode . AllDeviceInputAlwaysGoesToGameView & & updateType = = InputUpdateType . Editor )
#endif
)
#if UNITY_EDITOR
// When the game is playing and has focus, we never process input in editor updates. All we
// do is just switch to editor state buffers and then exit.
| | ( gameIsPlaying & & gameHasFocus & & updateType = = InputUpdateType . Editor )
#endif
) ;
2023-05-07 18:43:11 -04:00
bool dropStatusEvents = false ;
#if UNITY_EDITOR
if ( ! gameIsPlaying & & gameShouldGetInputRegardlessOfFocus & & ( eventBuffer . sizeInBytes > ( 100 * 1024 ) ) )
{
// If the game is not playing but we're sending all input events to the game, the buffer can just grow unbounded.
// So, in that case, set a flag to say we'd like to drop status events, and do not early out.
canEarlyOut = false ;
dropStatusEvents = true ;
}
#endif
2023-03-28 13:24:16 -04:00
if ( canEarlyOut )
{
// Normally, we process action timeouts after first processing all events. If we have no
// events, we still need to check timeouts.
if ( shouldProcessActionTimeouts )
ProcessStateChangeMonitorTimeouts ( ) ;
Profiler . EndSample ( ) ;
InvokeAfterUpdateCallback ( updateType ) ;
if ( canFlushBuffer )
eventBuffer . Reset ( ) ;
m_CurrentUpdate = default ;
return ;
}
var processingStartTime = Stopwatch . GetTimestamp ( ) ;
var totalEventLag = 0.0 ;
#if UNITY_EDITOR
var isPlaying = gameIsPlaying ;
#endif
try
{
m_InputEventStream = new InputEventStream ( ref eventBuffer , m_Settings . maxQueuedEventsPerUpdate ) ;
var totalEventBytesProcessed = 0 U ;
InputEvent * skipEventMergingFor = null ;
// Handle events.
while ( m_InputEventStream . remainingEventCount > 0 )
{
if ( m_Settings . maxEventBytesPerUpdate > 0 & &
totalEventBytesProcessed > = m_Settings . maxEventBytesPerUpdate )
{
Debug . LogError (
"Exceeded budget for maximum input event throughput per InputSystem.Update(). Discarding remaining events. "
+ "Increase InputSystem.settings.maxEventBytesPerUpdate or set it to 0 to remove the limit." ) ;
break ;
}
InputDevice device = null ;
var currentEventReadPtr = m_InputEventStream . currentEventPtr ;
Debug . Assert ( ! currentEventReadPtr - > handled , "Event in buffer is already marked as handled" ) ;
// In before render updates, we only take state events and only those for devices
// that have before render updates enabled.
if ( updateType = = InputUpdateType . BeforeRender )
{
while ( m_InputEventStream . remainingEventCount > 0 )
{
Debug . Assert ( ! currentEventReadPtr - > handled ,
"Iterated to event in buffer that is already marked as handled" ) ;
device = TryGetDeviceById ( currentEventReadPtr - > deviceId ) ;
if ( device ! = null & & device . updateBeforeRender & &
( currentEventReadPtr - > type = = StateEvent . Type | |
currentEventReadPtr - > type = = DeltaStateEvent . Type ) )
break ;
currentEventReadPtr = m_InputEventStream . Advance ( leaveEventInBuffer : true ) ;
}
}
if ( m_InputEventStream . remainingEventCount = = 0 )
break ;
var currentEventTimeInternal = currentEventReadPtr - > internalTime ;
var currentEventType = currentEventReadPtr - > type ;
2023-05-07 18:43:11 -04:00
#if UNITY_EDITOR
if ( dropStatusEvents )
{
// If the type here is a status event, ask advance not to leave the event in the buffer. Otherwise, leave it there.
if ( currentEventType = = StateEvent . Type | | currentEventType = = DeltaStateEvent . Type | | currentEventType = = IMECompositionEvent . Type )
m_InputEventStream . Advance ( false ) ;
else
m_InputEventStream . Advance ( true ) ;
continue ;
}
#endif
2023-03-28 13:24:16 -04:00
// In the editor, we discard all input events that occur in-between exiting edit mode and having
// entered play mode as otherwise we'll spill a bunch of UI events that have occurred while the
// UI was sort of neither in this mode nor in that mode. This would usually lead to the game receiving
// an accumulation of spurious inputs right in one of its first updates.
//
// NOTE: There's a chance the solution here will prove inadequate on the long run. We may do things
// here such as throwing partial touches away and then letting the rest of a touch go through.
// Could be that ultimately we need to issue a full reset of all devices at the beginning of
// play mode in the editor.
#if UNITY_EDITOR
if ( ( currentEventType = = StateEvent . Type | |
currentEventType = = DeltaStateEvent . Type ) & &
( updateType & InputUpdateType . Editor ) = = 0 & &
InputSystem . s_SystemObject . exitEditModeTime > 0 & &
currentEventTimeInternal > = InputSystem . s_SystemObject . exitEditModeTime & &
( currentEventTimeInternal < InputSystem . s_SystemObject . enterPlayModeTime | |
InputSystem . s_SystemObject . enterPlayModeTime = = 0 ) )
{
m_InputEventStream . Advance ( false ) ;
continue ;
}
#endif
// If we're timeslicing, check if the event time is within limits.
if ( timesliceEvents & & currentEventTimeInternal > = currentTime )
{
m_InputEventStream . Advance ( true ) ;
continue ;
}
// If we can't find the device, ignore the event.
if ( device = = null )
device = TryGetDeviceById ( currentEventReadPtr - > deviceId ) ;
if ( device = = null )
{
#if UNITY_EDITOR
////TODO: see if this is a device we haven't created and if so, just ignore
m_Diagnostics ? . OnCannotFindDeviceForEvent ( new InputEventPtr ( currentEventReadPtr ) ) ;
#endif
m_InputEventStream . Advance ( false ) ;
continue ;
}
// In the editor, we may need to bump events from editor updates into player updates
// and vice versa.
#if UNITY_EDITOR
if ( isPlaying & & ! gameHasFocus )
{
if ( m_Settings . editorInputBehaviorInPlayMode = = InputSettings . EditorInputBehaviorInPlayMode
. PointersAndKeyboardsRespectGameViewFocus & &
m_Settings . backgroundBehavior ! =
InputSettings . BackgroundBehavior . ResetAndDisableAllDevices )
{
var isPointerOrKeyboard = device is Pointer | | device is Keyboard ;
if ( updateType ! = InputUpdateType . Editor )
{
// Let everything but pointer and keyboard input through.
// If the event is from a pointer or keyboard, leave it in the buffer so it can be dealt with
// in a subsequent editor update. Otherwise, take it out.
if ( isPointerOrKeyboard )
{
m_InputEventStream . Advance ( true ) ;
continue ;
}
}
else
{
// Let only pointer and keyboard input through.
if ( ! isPointerOrKeyboard )
{
m_InputEventStream . Advance ( true ) ;
continue ;
}
}
}
}
#endif
// If device is disabled, we let the event through only in certain cases.
// Removal and configuration change events should always be processed.
if ( ! device . enabled & &
currentEventType ! = DeviceRemoveEvent . Type & &
currentEventType ! = DeviceConfigurationEvent . Type & &
( device . m_DeviceFlags & ( InputDevice . DeviceFlags . DisabledInRuntime |
InputDevice . DeviceFlags . DisabledWhileInBackground ) ) ! = 0 )
{
#if UNITY_EDITOR
// If the device is disabled in the backend, getting events for them
// is something that indicates a problem in the backend so diagnose.
if ( ( device . m_DeviceFlags & InputDevice . DeviceFlags . DisabledInRuntime ) ! = 0 )
m_Diagnostics ? . OnEventForDisabledDevice ( currentEventReadPtr , device ) ;
#endif
m_InputEventStream . Advance ( false ) ;
continue ;
}
// Check if the device wants to merge successive events.
if ( ! settings . disableRedundantEventsMerging & & device . hasEventMerger & & currentEventReadPtr ! = skipEventMergingFor )
{
// NOTE: This relies on events in the buffer being consecutive for the same device. This is not
// necessarily the case for events coming in from the background event queue where parallel
// producers may create interleaved input sequences. This will be fixed once we have the
// new buffering scheme for input events working in the native runtime.
var nextEvent = m_InputEventStream . Peek ( ) ;
// If there is next event after current one.
if ( ( nextEvent ! = null )
// And if next event is for the same device.
& & ( currentEventReadPtr - > deviceId = = nextEvent - > deviceId )
// And if next event is in the same timeslicing slot.
& & ( timesliceEvents ? ( nextEvent - > internalTime < currentTime ) : true )
)
{
// Then try to merge current event into next event.
if ( ( ( IEventMerger ) device ) . MergeForward ( currentEventReadPtr , nextEvent ) )
{
// And if succeeded, skip current event, as it was merged into next event.
m_InputEventStream . Advance ( false ) ;
continue ;
}
// If we can't merge current event with next one for any reason, we assume the next event
// carries crucial entropy (button changed state, phase changed, counter changed, etc).
// Hence semantic meaning for current event is "can't merge current with next because next is different".
// But semantic meaning for next event is "next event carries important information and should be preserved",
// from that point of view next event should not be merged with current nor with _next after next_ event.
//
// For example, given such stream of events:
// Mouse Mouse Mouse Mouse Mouse Mouse Mouse
// Event no1 Event no2 Event no3 Event no4 Event no5 Event no6 Event no7
// Time 1 Time 2 Time 3 Time 4 Time 5 Time 6 Time 7
// Pos(10,20) Pos(12,21) Pos(13,23) Pos(14,24) Pos(16,25) Pos(17,27) Pos(18,28)
// Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1)
// BtnLeft(0) BtnLeft(0) BtnLeft(0) BtnLeft(1) BtnLeft(1) BtnLeft(1) BtnLeft(1)
//
// if we then merge without skipping next event here:
// Mouse Mouse
// Event no3 Event no7
// Time 3 Time 7
// Pos(13,23) Pos(18,28)
// Delta(4,4) Delta(5,5)
// BtnLeft(0) BtnLeft(1)
//
// As you can see, the event no4 containing mouse button press was lost,
// and with it we lose the important information of timestamp of mouse button press.
//
// With skipping merging next event we will get:
// Mouse Mouse Mouse
// Time 3 Time 4 Time 7
// Event no3 Event no4 Event no7
// Pos(13,23) Pos(14,24) Pos(18,28)
// Delta(3,3) Delta(1,1) Delta(4,4)
// BtnLeft(0) BtnLeft(1) BtnLeft(1)
//
// And no4 is preserved, with the exact timestamp of button press.
skipEventMergingFor = nextEvent ;
}
}
// Give the device a chance to do something with data before we propagate it to event listeners.
if ( device . hasEventPreProcessor )
{
#if UNITY_EDITOR
var eventSizeBeforePreProcessor = currentEventReadPtr - > sizeInBytes ;
#endif
var shouldProcess = ( ( IEventPreProcessor ) device ) . PreProcessEvent ( currentEventReadPtr ) ;
#if UNITY_EDITOR
if ( currentEventReadPtr - > sizeInBytes > eventSizeBeforePreProcessor )
throw new AccessViolationException ( $"'{device}'.PreProcessEvent tries to grow an event from {eventSizeBeforePreProcessor} bytes to {currentEventReadPtr->sizeInBytes} bytes, this will potentially corrupt events after the current event and/or cause out-of-bounds memory access." ) ;
#endif
if ( ! shouldProcess )
{
// Skip event if PreProcessEvent considers it to be irrelevant.
m_InputEventStream . Advance ( false ) ;
continue ;
}
}
// Give listeners a shot at the event.
// NOTE: We call listeners also for events where the device is disabled. This is crucial for code
// such as TouchSimulation that disables the originating devices and then uses its events to
// create simulated events from.
if ( m_EventListeners . length > 0 )
{
DelegateHelpers . InvokeCallbacksSafe ( ref m_EventListeners ,
new InputEventPtr ( currentEventReadPtr ) , device , "InputSystem.onEvent" ) ;
// If a listener marks the event as handled, we don't process it further.
if ( currentEventReadPtr - > handled )
{
m_InputEventStream . Advance ( false ) ;
continue ;
}
}
// Update metrics.
if ( currentEventTimeInternal < = currentTime )
totalEventLag + = currentTime - currentEventTimeInternal ;
+ + m_Metrics . totalEventCount ;
m_Metrics . totalEventBytes + = ( int ) currentEventReadPtr - > sizeInBytes ;
// Process.
switch ( currentEventType )
{
case StateEvent . Type :
case DeltaStateEvent . Type :
var eventPtr = new InputEventPtr ( currentEventReadPtr ) ;
// Ignore the event if the last state update we received for the device was
// newer than this state event is. We don't allow devices to go back in time.
//
// NOTE: We make an exception here for devices that implement IInputStateCallbackReceiver (such
// as Touchscreen). For devices that dynamically incorporate state it can be hard ensuring
// a global ordering of events as there may be multiple substreams (e.g. each individual touch)
// that are generated in the backend and would require considerable work to ensure monotonically
// increasing timestamps across all such streams.
var deviceIsStateCallbackReceiver = device . hasStateCallbacks ;
if ( currentEventTimeInternal < device . m_LastUpdateTimeInternal & &
! ( deviceIsStateCallbackReceiver & & device . stateBlock . format ! = eventPtr . stateFormat ) )
{
#if UNITY_EDITOR
m_Diagnostics ? . OnEventTimestampOutdated ( new InputEventPtr ( currentEventReadPtr ) , device ) ;
#endif
break ;
}
// Update the state of the device from the event. If the device is an IInputStateCallbackReceiver,
// let the device handle the event. If not, we do it ourselves.
var haveChangedStateOtherThanNoise = true ;
if ( deviceIsStateCallbackReceiver )
{
2023-05-07 18:43:11 -04:00
m_ShouldMakeCurrentlyUpdatingDeviceCurrent = true ;
2023-03-28 13:24:16 -04:00
// NOTE: We leave it to the device to make sure the event has the right format. This allows the
// device to handle multiple different incoming formats.
( ( IInputStateCallbackReceiver ) device ) . OnStateEvent ( eventPtr ) ;
2023-05-07 18:43:11 -04:00
haveChangedStateOtherThanNoise = m_ShouldMakeCurrentlyUpdatingDeviceCurrent ;
2023-03-28 13:24:16 -04:00
}
else
{
// If the state format doesn't match, ignore the event.
if ( device . stateBlock . format ! = eventPtr . stateFormat )
{
#if UNITY_EDITOR
m_Diagnostics ? . OnEventFormatMismatch ( currentEventReadPtr , device ) ;
#endif
break ;
}
haveChangedStateOtherThanNoise = UpdateState ( device , eventPtr , updateType ) ;
}
totalEventBytesProcessed + = eventPtr . sizeInBytes ;
// Update timestamp on device.
// NOTE: We do this here and not in UpdateState() so that InputState.Change() will *NOT* change timestamps.
// Only events should. If running play mode updates in editor, we want to defer to the play mode
// callbacks to set the last update time to avoid dropping events only processed by the editor state.
if ( device . m_LastUpdateTimeInternal < = eventPtr . internalTime
#if UNITY_EDITOR
& & ! ( updateType = = InputUpdateType . Editor & & runPlayerUpdatesInEditMode )
#endif
)
device . m_LastUpdateTimeInternal = eventPtr . internalTime ;
// Make device current. Again, only do this when receiving events.
if ( haveChangedStateOtherThanNoise )
device . MakeCurrent ( ) ;
break ;
case TextEvent . Type :
{
var textEventPtr = ( TextEvent * ) currentEventReadPtr ;
if ( device is ITextInputReceiver textInputReceiver )
{
var utf32Char = textEventPtr - > character ;
if ( utf32Char > = 0x10000 )
{
// Send surrogate pair.
utf32Char - = 0x10000 ;
var highSurrogate = 0xD800 + ( ( utf32Char > > 10 ) & 0x3FF ) ;
var lowSurrogate = 0xDC00 + ( utf32Char & 0x3FF ) ;
textInputReceiver . OnTextInput ( ( char ) highSurrogate ) ;
textInputReceiver . OnTextInput ( ( char ) lowSurrogate ) ;
}
else
{
// Send single, plain character.
textInputReceiver . OnTextInput ( ( char ) utf32Char ) ;
}
}
break ;
}
case IMECompositionEvent . Type :
{
var imeEventPtr = ( IMECompositionEvent * ) currentEventReadPtr ;
var textInputReceiver = device as ITextInputReceiver ;
textInputReceiver ? . OnIMECompositionChanged ( imeEventPtr - > compositionString ) ;
break ;
}
case DeviceRemoveEvent . Type :
{
RemoveDevice ( device , keepOnListOfAvailableDevices : false ) ;
// If it's a native device with a description, put it on the list of disconnected
// devices.
if ( device . native & & ! device . description . empty )
{
ArrayHelpers . AppendWithCapacity ( ref m_DisconnectedDevices ,
ref m_DisconnectedDevicesCount , device ) ;
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceChangeListeners ,
device , InputDeviceChange . Disconnected , "InputSystem.onDeviceChange" ) ;
}
break ;
}
case DeviceConfigurationEvent . Type :
device . NotifyConfigurationChanged ( ) ;
InputActionState . OnDeviceChange ( device , InputDeviceChange . ConfigurationChanged ) ;
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceChangeListeners ,
device , InputDeviceChange . ConfigurationChanged , "InputSystem.onDeviceChange" ) ;
break ;
case DeviceResetEvent . Type :
ResetDevice ( device ,
alsoResetDontResetControls : ( ( DeviceResetEvent * ) currentEventReadPtr ) - > hardReset ) ;
break ;
}
m_InputEventStream . Advance ( leaveEventInBuffer : false ) ;
}
m_Metrics . totalEventProcessingTime + =
( ( double ) ( Stopwatch . GetTimestamp ( ) - processingStartTime ) ) / Stopwatch . Frequency ;
m_Metrics . totalEventLagTime + = totalEventLag ;
m_InputEventStream . Close ( ref eventBuffer ) ;
}
catch ( Exception )
{
// We need to restore m_InputEventStream to a sound state
// to avoid failing recursive OnUpdate check next frame.
m_InputEventStream . CleanUpAfterException ( ) ;
throw ;
}
if ( shouldProcessActionTimeouts )
ProcessStateChangeMonitorTimeouts ( ) ;
Profiler . EndSample ( ) ;
////FIXME: need to ensure that if someone calls QueueEvent() from an onAfterUpdate callback, we don't end up with a
//// mess in the event buffer
//// same goes for events that someone may queue from a change monitor callback
InvokeAfterUpdateCallback ( updateType ) ;
m_CurrentUpdate = default ;
}
private void InvokeAfterUpdateCallback ( InputUpdateType updateType )
{
// don't invoke the after update callback if this is an editor update and the game is playing. We
// skip event processing when playing in the editor and the game has focus, which means that any
// handlers for this delegate that query input state during this update will get no values.
if ( updateType = = InputUpdateType . Editor & & gameIsPlaying )
return ;
DelegateHelpers . InvokeCallbacksSafe ( ref m_AfterUpdateListeners ,
"InputSystem.onAfterUpdate" ) ;
}
2023-05-07 18:43:11 -04:00
private bool m_ShouldMakeCurrentlyUpdatingDeviceCurrent ;
// This is a dirty hot fix to expose entropy from device back to input manager to make a choice if we want to make device current or not.
// A proper fix would be to change IInputStateCallbackReceiver.OnStateEvent to return bool to make device current or not.
internal void DontMakeCurrentlyUpdatingDeviceCurrent ( )
{
m_ShouldMakeCurrentlyUpdatingDeviceCurrent = false ;
}
2023-03-28 13:24:16 -04:00
internal unsafe bool UpdateState ( InputDevice device , InputEvent * eventPtr , InputUpdateType updateType )
{
Debug . Assert ( eventPtr ! = null , "Received NULL event ptr" ) ;
var stateBlockOfDevice = device . m_StateBlock ;
var stateBlockSizeOfDevice = stateBlockOfDevice . sizeInBits / 8 ; // Always byte-aligned; avoid calling alignedSizeInBytes.
var offsetInDeviceStateToCopyTo = 0 u ;
uint sizeOfStateToCopy ;
uint receivedStateSize ;
byte * ptrToReceivedState ;
FourCC receivedStateFormat ;
// Grab state data from event and decide where to copy to and how much to copy.
if ( eventPtr - > type = = StateEvent . Type )
{
var stateEventPtr = ( StateEvent * ) eventPtr ;
receivedStateFormat = stateEventPtr - > stateFormat ;
receivedStateSize = stateEventPtr - > stateSizeInBytes ;
ptrToReceivedState = ( byte * ) stateEventPtr - > state ;
// Ignore extra state at end of event.
sizeOfStateToCopy = receivedStateSize ;
if ( sizeOfStateToCopy > stateBlockSizeOfDevice )
sizeOfStateToCopy = stateBlockSizeOfDevice ;
}
else
{
Debug . Assert ( eventPtr - > type = = DeltaStateEvent . Type , "Given event must either be a StateEvent or a DeltaStateEvent" ) ;
var deltaEventPtr = ( DeltaStateEvent * ) eventPtr ;
receivedStateFormat = deltaEventPtr - > stateFormat ;
receivedStateSize = deltaEventPtr - > deltaStateSizeInBytes ;
ptrToReceivedState = ( byte * ) deltaEventPtr - > deltaState ;
offsetInDeviceStateToCopyTo = deltaEventPtr - > stateOffset ;
// Ignore extra state at end of event.
sizeOfStateToCopy = receivedStateSize ;
if ( offsetInDeviceStateToCopyTo + sizeOfStateToCopy > stateBlockSizeOfDevice )
{
if ( offsetInDeviceStateToCopyTo > = stateBlockSizeOfDevice )
return false ; // Entire delta state is out of range.
sizeOfStateToCopy = stateBlockSizeOfDevice - offsetInDeviceStateToCopyTo ;
}
}
Debug . Assert ( device . m_StateBlock . format = = receivedStateFormat , "Received state format does not match format of device" ) ;
// Write state.
return UpdateState ( device , updateType , ptrToReceivedState , offsetInDeviceStateToCopyTo ,
sizeOfStateToCopy , eventPtr - > internalTime , eventPtr ) ;
}
/// <summary>
/// This method is the workhorse for updating input state in the system. It runs all the logic of incorporating
/// new state into devices and triggering whatever change monitors are attached to the state memory that gets
/// touched.
/// </summary>
/// <remarks>
/// This method can be invoked from outside the event processing loop and the given data does not have to come
/// from an event.
///
/// This method does NOT respect <see cref="IInputStateCallbackReceiver"/>. This means that the device will
/// NOT get a shot at intervening in the state write.
/// </remarks>
/// <param name="device">Device to update state on. <paramref name="stateOffsetInDevice"/> is relative to device's
/// starting offset in memory.</param>
/// <param name="eventPtr">Pointer to state event from which the state change was initiated. Null if the state
/// change is not coming from an event.</param>
internal unsafe bool UpdateState ( InputDevice device , InputUpdateType updateType ,
void * statePtr , uint stateOffsetInDevice , uint stateSize , double internalTime , InputEventPtr eventPtr = default )
{
var deviceIndex = device . m_DeviceIndex ;
ref var stateBlockOfDevice = ref device . m_StateBlock ;
////TODO: limit stateSize and StateOffset by the device's state memory
var deviceBuffer = ( byte * ) InputStateBuffers . GetFrontBufferForDevice ( deviceIndex ) ;
// If state monitors need to be re-sorted, do it now.
// NOTE: This must happen with the monitors in non-signalled state!
SortStateChangeMonitorsIfNecessary ( deviceIndex ) ;
// Before we update state, let change monitors compare the old and the new state.
// We do this instead of first updating the front buffer and then comparing to the
// back buffer as that would require a buffer flip for each state change in order
// for the monitors to work reliably. By comparing the *event* data to the current
// state, we can have multiple state events in the same frame yet still get reliable
// change notifications.
var haveSignalledMonitors =
ProcessStateChangeMonitors ( deviceIndex , statePtr ,
deviceBuffer + stateBlockOfDevice . byteOffset ,
stateSize , stateOffsetInDevice ) ;
var deviceStateOffset = device . m_StateBlock . byteOffset + stateOffsetInDevice ;
var deviceStatePtr = deviceBuffer + deviceStateOffset ;
////REVIEW: Should we do this only for events but not for InputState.Change()?
// If noise filtering on .current is turned on and the device may have noise,
// determine if the event carries signal or not.
var noiseMask = device . noisy
? ( byte * ) InputStateBuffers . s_NoiseMaskBuffer + deviceStateOffset
: null ;
// Compare the current state of the device to the newly received state but overlay
// the comparison by the noise mask.
var makeDeviceCurrent = ! MemoryHelpers . MemCmpBitRegion ( deviceStatePtr , statePtr ,
0 , stateSize * 8 , mask : noiseMask ) ;
// Buffer flip.
var flipped = FlipBuffersForDeviceIfNecessary ( device , updateType ) ;
// Now write the state.
#if UNITY_EDITOR
if ( updateType = = InputUpdateType . Editor )
{
WriteStateChange ( m_StateBuffers . m_EditorStateBuffers , deviceIndex , ref stateBlockOfDevice , stateOffsetInDevice ,
statePtr , stateSize , flipped ) ;
}
else
#endif
{
WriteStateChange ( m_StateBuffers . m_PlayerStateBuffers , deviceIndex , ref stateBlockOfDevice ,
stateOffsetInDevice , statePtr , stateSize , flipped ) ;
}
// Notify listeners.
DelegateHelpers . InvokeCallbacksSafe ( ref m_DeviceStateChangeListeners ,
device , eventPtr , "InputSystem.onDeviceStateChange" ) ;
// Now that we've committed the new state to memory, if any of the change
// monitors fired, let the associated actions know.
if ( haveSignalledMonitors )
FireStateChangeNotifications ( deviceIndex , internalTime , eventPtr ) ;
return makeDeviceCurrent ;
}
2023-05-07 18:43:11 -04:00
private unsafe void WriteStateChange ( InputStateBuffers . DoubleBuffers buffers , int deviceIndex ,
2023-03-28 13:24:16 -04:00
ref InputStateBlock deviceStateBlock , uint stateOffsetInDevice , void * statePtr , uint stateSizeInBytes , bool flippedBuffers )
{
var frontBuffer = buffers . GetFrontBuffer ( deviceIndex ) ;
Debug . Assert ( frontBuffer ! = null ) ;
// If we're updating less than the full state, we need to preserve the parts we are not updating.
// Instead of trying to optimize here and only copy what we really need, we just go and copy the
// entire state of the device over.
//
// NOTE: This copying must only happen once, right after a buffer flip. Otherwise we may copy old,
// stale input state from the back buffer over state that has already been updated with newer
// data.
var deviceStateSize = deviceStateBlock . sizeInBits / 8 ; // Always byte-aligned; avoid calling alignedSizeInBytes.
if ( flippedBuffers & & deviceStateSize ! = stateSizeInBytes )
{
var backBuffer = buffers . GetBackBuffer ( deviceIndex ) ;
Debug . Assert ( backBuffer ! = null ) ;
UnsafeUtility . MemCpy (
( byte * ) frontBuffer + deviceStateBlock . byteOffset ,
( byte * ) backBuffer + deviceStateBlock . byteOffset ,
deviceStateSize ) ;
}
2023-05-07 18:43:11 -04:00
if ( InputSettings . readValueCachingFeatureEnabled )
{
// if the buffers have just been flipped, and we're doing a full state update, then the state from the
// previous update is now in the back buffer, and we should be comparing to that when checking what
// controls have changed
var buffer = ( byte * ) frontBuffer ;
if ( flippedBuffers & & deviceStateSize = = stateSizeInBytes )
buffer = ( byte * ) buffers . GetBackBuffer ( deviceIndex ) ;
m_Devices [ deviceIndex ] . WriteChangedControlStates ( buffer + deviceStateBlock . byteOffset , statePtr ,
stateSizeInBytes , stateOffsetInDevice ) ;
}
2023-03-28 13:24:16 -04:00
UnsafeUtility . MemCpy ( ( byte * ) frontBuffer + deviceStateBlock . byteOffset + stateOffsetInDevice , statePtr ,
stateSizeInBytes ) ;
}
// Flip front and back buffer for device, if necessary. May flip buffers for more than just
// the given update type.
// Returns true if there was a buffer flip.
private bool FlipBuffersForDeviceIfNecessary ( InputDevice device , InputUpdateType updateType )
{
if ( updateType = = InputUpdateType . BeforeRender )
{
////REVIEW: I think this is wrong; if we haven't flipped in the current dynamic or fixed update, we should do so now
// We never flip buffers for before render. Instead, we already write
// into the front buffer.
return false ;
}
#if UNITY_EDITOR
////REVIEW: should this use the editor update ticks as quasi-frame-boundaries?
// Updates go to the editor only if the game isn't playing or does not have focus.
// Otherwise we fall through to the logic that flips for the *next* dynamic and
// fixed updates.
if ( updateType = = InputUpdateType . Editor )
{
////REVIEW: This isn't right. The editor does have update ticks which constitute the equivalent of player frames.
// The editor doesn't really have a concept of frame-to-frame operation the
// same way the player does. So we simply flip buffers on a device whenever
// a new state event for it comes in.
m_StateBuffers . m_EditorStateBuffers . SwapBuffers ( device . m_DeviceIndex ) ;
return true ;
}
#endif
// Flip buffers if we haven't already for this frame.
if ( device . m_CurrentUpdateStepCount ! = InputUpdate . s_UpdateStepCount )
{
m_StateBuffers . m_PlayerStateBuffers . SwapBuffers ( device . m_DeviceIndex ) ;
device . m_CurrentUpdateStepCount = InputUpdate . s_UpdateStepCount ;
return true ;
}
return false ;
}
// Domain reload survival logic. Also used for pushing and popping input system
// state for testing.
// Stuff everything that we want to survive a domain reload into
// a m_SerializedState.
#if UNITY_EDITOR | | DEVELOPMENT_BUILD
[Serializable]
internal struct DeviceState
{
// Preserving InputDevices is somewhat tricky business. Serializing
// them in full would involve pretty nasty work. We have the restriction,
// however, that everything needs to be created from layouts (it partly
// exists for the sake of reload survivability), so we should be able to
// just go and recreate the device from the layout. This also has the
// advantage that if the layout changes between reloads, the change
// automatically takes effect.
public string name ;
public string layout ;
public string variants ;
public string [ ] usages ;
public int deviceId ;
public int participantId ;
public InputDevice . DeviceFlags flags ;
public InputDeviceDescription description ;
public void Restore ( InputDevice device )
{
var usageCount = usages . LengthSafe ( ) ;
for ( var i = 0 ; i < usageCount ; + + i )
device . AddDeviceUsage ( new InternedString ( usages [ i ] ) ) ;
device . m_ParticipantId = participantId ;
}
}
/// <summary>
/// State we take across domain reloads.
/// </summary>
/// <remarks>
/// Most of the state we re-recreate in-between reloads and do not store
/// in this structure. In particular, we do not preserve anything from
/// the various RegisterXXX().
///
/// WARNING
///
/// Making changes to serialized data format will likely to break upgrading projects from older versions.
/// That is until you restart the editor, then we recreate everything from clean state.
/// </remarks>
[Serializable]
internal struct SerializedState
{
public int layoutRegistrationVersion ;
public float pollingFrequency ;
public DeviceState [ ] devices ;
public AvailableDevice [ ] availableDevices ;
public InputStateBuffers buffers ;
public InputUpdate . SerializedState updateState ;
public InputUpdateType updateMask ;
public InputMetrics metrics ;
public InputSettings settings ;
#if UNITY_ANALYTICS | | UNITY_EDITOR
public bool haveSentStartupAnalytics ;
#endif
}
internal SerializedState SaveState ( )
{
// Devices.
var deviceCount = m_DevicesCount ;
var deviceArray = new DeviceState [ deviceCount ] ;
for ( var i = 0 ; i < deviceCount ; + + i )
{
var device = m_Devices [ i ] ;
string [ ] usages = null ;
if ( device . usages . Count > 0 )
usages = device . usages . Select ( x = > x . ToString ( ) ) . ToArray ( ) ;
var deviceState = new DeviceState
{
name = device . name ,
layout = device . layout ,
variants = device . variants ,
deviceId = device . deviceId ,
participantId = device . m_ParticipantId ,
usages = usages ,
description = device . m_Description ,
flags = device . m_DeviceFlags
} ;
deviceArray [ i ] = deviceState ;
}
return new SerializedState
{
layoutRegistrationVersion = m_LayoutRegistrationVersion ,
pollingFrequency = m_PollingFrequency ,
devices = deviceArray ,
availableDevices = m_AvailableDevices ? . Take ( m_AvailableDeviceCount ) . ToArray ( ) ,
buffers = m_StateBuffers ,
updateState = InputUpdate . Save ( ) ,
updateMask = m_UpdateMask ,
metrics = m_Metrics ,
settings = m_Settings ,
#if UNITY_ANALYTICS | | UNITY_EDITOR
haveSentStartupAnalytics = m_HaveSentStartupAnalytics ,
#endif
} ;
}
internal void RestoreStateWithoutDevices ( SerializedState state )
{
m_StateBuffers = state . buffers ;
m_LayoutRegistrationVersion = state . layoutRegistrationVersion + 1 ;
updateMask = state . updateMask ;
m_Metrics = state . metrics ;
m_PollingFrequency = state . pollingFrequency ;
if ( m_Settings ! = null )
Object . DestroyImmediate ( m_Settings ) ;
m_Settings = state . settings ;
#if UNITY_ANALYTICS | | UNITY_EDITOR
m_HaveSentStartupAnalytics = state . haveSentStartupAnalytics ;
#endif
////REVIEW: instead of accessing globals here, we could move this to when we re-create devices
// Update state.
InputUpdate . Restore ( state . updateState ) ;
}
// If these are set, we clear them out on the first input update.
internal DeviceState [ ] m_SavedDeviceStates ;
internal AvailableDevice [ ] m_SavedAvailableDevices ;
/// <summary>
/// Recreate devices based on the devices we had before a domain reload.
/// </summary>
/// <remarks>
/// Note that device indices may change between domain reloads.
///
/// We recreate devices using the layout information as it exists now as opposed to
/// as it existed before the domain reload. This means we'll be picking up any changes that
/// have happened to layouts as part of the reload (including layouts having been removed
/// entirely).
/// </remarks>
internal void RestoreDevicesAfterDomainReload ( )
{
Profiler . BeginSample ( "InputManager.RestoreDevicesAfterDomainReload" ) ;
using ( InputDeviceBuilder . Ref ( ) )
{
DeviceState [ ] retainedDeviceStates = null ;
var deviceStates = m_SavedDeviceStates ;
var deviceCount = m_SavedDeviceStates . LengthSafe ( ) ;
m_SavedDeviceStates = null ; // Prevent layout matcher registering themselves on the fly from picking anything off this list.
for ( var i = 0 ; i < deviceCount ; + + i )
{
ref var deviceState = ref deviceStates [ i ] ;
var device = TryGetDeviceById ( deviceState . deviceId ) ;
if ( device ! = null )
continue ;
var layout = TryFindMatchingControlLayout ( ref deviceState . description ,
deviceState . deviceId ) ;
if ( layout . IsEmpty ( ) )
{
var previousLayout = new InternedString ( deviceState . layout ) ;
if ( m_Layouts . HasLayout ( previousLayout ) )
layout = previousLayout ;
}
if ( layout . IsEmpty ( ) | | ! RestoreDeviceFromSavedState ( ref deviceState , layout ) )
ArrayHelpers . Append ( ref retainedDeviceStates , deviceState ) ;
}
// See if we can make sense of an available device now that we couldn't make sense of
// before. This can be the case if there's new layout information that wasn't available
// before.
if ( m_SavedAvailableDevices ! = null )
{
m_AvailableDevices = m_SavedAvailableDevices ;
m_AvailableDeviceCount = m_SavedAvailableDevices . LengthSafe ( ) ;
for ( var i = 0 ; i < m_AvailableDeviceCount ; + + i )
{
var device = TryGetDeviceById ( m_AvailableDevices [ i ] . deviceId ) ;
if ( device ! = null )
continue ;
if ( m_AvailableDevices [ i ] . isRemoved )
continue ;
var layout = TryFindMatchingControlLayout ( ref m_AvailableDevices [ i ] . description ,
m_AvailableDevices [ i ] . deviceId ) ;
if ( ! layout . IsEmpty ( ) )
{
try
{
AddDevice ( layout , m_AvailableDevices [ i ] . deviceId ,
deviceDescription : m_AvailableDevices [ i ] . description ,
deviceFlags : m_AvailableDevices [ i ] . isNative ? InputDevice . DeviceFlags . Native : 0 ) ;
}
catch ( Exception )
{
// Just ignore. Simply means we still can't really turn the device into something useful.
}
}
}
}
// Done. Discard saved arrays.
m_SavedDeviceStates = retainedDeviceStates ;
m_SavedAvailableDevices = null ;
}
Profiler . EndSample ( ) ;
}
// We have two general types of devices we need to care about when recreating devices
// after domain reloads:
//
// A) device with InputDeviceDescription
// B) device created directly from specific layout
//
// A) should go through the normal matching process whereas B) should get recreated with
// layout of same name (if still available).
//
// So we kick device recreation off from two points:
//
// 1) From RegisterControlLayoutMatcher to catch A)
// 2) From RegisterControlLayout to catch B)
//
// Additionally, we have the complication that a layout a device was using was something
// dynamically registered from onFindLayoutForDevice. We don't do anything special about that.
// The first full input update will flush out the list of saved device states and at that
// point, any onFindLayoutForDevice hooks simply have to be in place. If they are, devices
// will get recreated appropriately.
//
// It would be much simpler to recreate all devices as the first thing in the first full input
// update but that would mean that devices would become available only very late. They would
// not, for example, be available when MonoBehaviour.Start methods are invoked.
private bool RestoreDeviceFromSavedState ( ref DeviceState deviceState , InternedString layout )
{
// We assign the same device IDs here to newly created devices that they had
// before the domain reload. This is safe as device ID allocation is under the
// control of the runtime and not expected to be affected by a domain reload.
InputDevice device ;
try
{
device = AddDevice ( layout ,
deviceDescription : deviceState . description ,
deviceId : deviceState . deviceId ,
deviceName : deviceState . name ,
deviceFlags : deviceState . flags ,
variants : new InternedString ( deviceState . variants ) ) ;
}
catch ( Exception exception )
{
Debug . LogError (
$"Could not recreate input device '{deviceState.description}' with layout '{deviceState.layout}' and variants '{deviceState.variants}' after domain reload" ) ;
Debug . LogException ( exception ) ;
return true ; // Don't try again.
}
deviceState . Restore ( device ) ;
return true ;
}
#endif // UNITY_EDITOR || DEVELOPMENT_BUILD
}
}