Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /InputManagerStateMonitors.cs
| using System; | |
| using Unity.Collections.LowLevel.Unsafe; | |
| using UnityEngine.InputSystem.LowLevel; | |
| using UnityEngine.InputSystem.Utilities; | |
| namespace UnityEngine.InputSystem | |
| { | |
| internal partial class InputManager | |
| { | |
| // Indices correspond with those in m_Devices. | |
| internal StateChangeMonitorsForDevice[] m_StateChangeMonitors; | |
| private InlinedArray<StateChangeMonitorTimeout> m_StateChangeMonitorTimeouts; | |
| ////TODO: support combining monitors for bitfields | |
| public void AddStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex, uint groupIndex) | |
| { | |
| Debug.Assert(m_DevicesCount > 0); | |
| var device = control.device; | |
| var deviceIndex = device.m_DeviceIndex; | |
| Debug.Assert(deviceIndex != InputDevice.kInvalidDeviceIndex); | |
| // Allocate/reallocate monitor arrays, if necessary. | |
| // We lazy-sync it to array of devices. | |
| if (m_StateChangeMonitors == null) | |
| m_StateChangeMonitors = new StateChangeMonitorsForDevice[m_DevicesCount]; | |
| else if (m_StateChangeMonitors.Length <= deviceIndex) | |
| Array.Resize(ref m_StateChangeMonitors, m_DevicesCount); | |
| // If we have removed monitors | |
| if (!isProcessingEvents && m_StateChangeMonitors[deviceIndex].needToCompactArrays) | |
| m_StateChangeMonitors[deviceIndex].CompactArrays(); | |
| // Add record. | |
| m_StateChangeMonitors[deviceIndex].Add(control, monitor, monitorIndex, groupIndex); | |
| } | |
| private void RemoveStateChangeMonitors(InputDevice device) | |
| { | |
| if (m_StateChangeMonitors == null) | |
| return; | |
| var deviceIndex = device.m_DeviceIndex; | |
| Debug.Assert(deviceIndex != InputDevice.kInvalidDeviceIndex); | |
| if (deviceIndex >= m_StateChangeMonitors.Length) | |
| return; | |
| m_StateChangeMonitors[deviceIndex].Clear(); | |
| // Clear timeouts pending on any control on the device. | |
| for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i) | |
| if (m_StateChangeMonitorTimeouts[i].control?.device == device) | |
| m_StateChangeMonitorTimeouts[i] = default; | |
| } | |
| public void RemoveStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex) | |
| { | |
| if (m_StateChangeMonitors == null) | |
| return; | |
| var device = control.device; | |
| var deviceIndex = device.m_DeviceIndex; | |
| // Ignore if device has already been removed. | |
| if (deviceIndex == InputDevice.kInvalidDeviceIndex) | |
| return; | |
| // Ignore if there are no state monitors set up for the device. | |
| if (deviceIndex >= m_StateChangeMonitors.Length) | |
| return; | |
| m_StateChangeMonitors[deviceIndex].Remove(monitor, monitorIndex, isProcessingEvents); | |
| // Remove pending timeouts on the monitor. | |
| for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i) | |
| if (m_StateChangeMonitorTimeouts[i].monitor == monitor && | |
| m_StateChangeMonitorTimeouts[i].monitorIndex == monitorIndex) | |
| m_StateChangeMonitorTimeouts[i] = default; | |
| } | |
| public void AddStateChangeMonitorTimeout(InputControl control, IInputStateChangeMonitor monitor, double time, long monitorIndex, int timerIndex) | |
| { | |
| m_StateChangeMonitorTimeouts.Append( | |
| new StateChangeMonitorTimeout | |
| { | |
| control = control, | |
| time = time, | |
| monitor = monitor, | |
| monitorIndex = monitorIndex, | |
| timerIndex = timerIndex, | |
| }); | |
| } | |
| public void RemoveStateChangeMonitorTimeout(IInputStateChangeMonitor monitor, long monitorIndex, int timerIndex) | |
| { | |
| var timeoutCount = m_StateChangeMonitorTimeouts.length; | |
| for (var i = 0; i < timeoutCount; ++i) | |
| { | |
| ////REVIEW: can we avoid the repeated array lookups without copying the struct out? | |
| if (ReferenceEquals(m_StateChangeMonitorTimeouts[i].monitor, monitor) | |
| && m_StateChangeMonitorTimeouts[i].monitorIndex == monitorIndex | |
| && m_StateChangeMonitorTimeouts[i].timerIndex == timerIndex) | |
| { | |
| m_StateChangeMonitorTimeouts[i] = default; | |
| break; | |
| } | |
| } | |
| } | |
| private void SortStateChangeMonitorsIfNecessary(int deviceIndex) | |
| { | |
| if (m_StateChangeMonitors != null && deviceIndex < m_StateChangeMonitors.Length && | |
| m_StateChangeMonitors[deviceIndex].needToUpdateOrderingOfMonitors) | |
| m_StateChangeMonitors[deviceIndex].SortMonitorsByIndex(); | |
| } | |
| public void SignalStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor) | |
| { | |
| var device = control.device; | |
| var deviceIndex = device.m_DeviceIndex; | |
| ref var monitorsForDevice = ref m_StateChangeMonitors[deviceIndex]; | |
| for (var i = 0; i < monitorsForDevice.signalled.length; ++i) | |
| { | |
| SortStateChangeMonitorsIfNecessary(i); | |
| ref var listener = ref monitorsForDevice.listeners[i]; | |
| if (listener.control == control && listener.monitor == monitor) | |
| monitorsForDevice.signalled.SetBit(i); | |
| } | |
| } | |
| public unsafe void FireStateChangeNotifications() | |
| { | |
| var time = m_Runtime.currentTime; | |
| var count = Math.Min(m_StateChangeMonitors.LengthSafe(), m_DevicesCount); | |
| for (var i = 0; i < count; ++i) | |
| FireStateChangeNotifications(i, time, null); | |
| } | |
| // Record for a timeout installed on a state change monitor. | |
| private struct StateChangeMonitorTimeout | |
| { | |
| public InputControl control; | |
| public double time; | |
| public IInputStateChangeMonitor monitor; | |
| public long monitorIndex; | |
| public int timerIndex; | |
| } | |
| // Maps a single control to an action interested in the control. If | |
| // multiple actions are interested in the same control, we will end up | |
| // processing the control repeatedly but we assume this is the exception | |
| // and so optimize for the case where there's only one action going to | |
| // a control. | |
| // | |
| // Split into two structures to keep data needed only when there is an | |
| // actual value change out of the data we need for doing the scanning. | |
| internal struct StateChangeMonitorListener | |
| { | |
| public InputControl control; | |
| public IInputStateChangeMonitor monitor; | |
| public long monitorIndex; | |
| public uint groupIndex; | |
| } | |
| internal struct StateChangeMonitorsForDevice | |
| { | |
| public MemoryHelpers.BitRegion[] memoryRegions; | |
| public StateChangeMonitorListener[] listeners; | |
| public DynamicBitfield signalled; | |
| public bool needToUpdateOrderingOfMonitors; | |
| public bool needToCompactArrays; | |
| public int count => signalled.length; | |
| public void Add(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex, uint groupIndex) | |
| { | |
| // NOTE: This method must only *append* to arrays. This way we can safely add data while traversing | |
| // the arrays in FireStateChangeNotifications. Note that appending *may* mean that the arrays | |
| // are switched to larger arrays. | |
| // Record listener. | |
| var listenerCount = signalled.length; | |
| ArrayHelpers.AppendWithCapacity(ref listeners, ref listenerCount, | |
| new StateChangeMonitorListener | |
| { monitor = monitor, monitorIndex = monitorIndex, groupIndex = groupIndex, control = control }); | |
| // Record memory region. | |
| ref var controlStateBlock = ref control.m_StateBlock; | |
| var memoryRegionCount = signalled.length; | |
| ArrayHelpers.AppendWithCapacity(ref memoryRegions, ref memoryRegionCount, | |
| new MemoryHelpers.BitRegion(controlStateBlock.byteOffset - control.device.stateBlock.byteOffset, | |
| controlStateBlock.bitOffset, controlStateBlock.sizeInBits)); | |
| signalled.SetLength(signalled.length + 1); | |
| needToUpdateOrderingOfMonitors = true; | |
| } | |
| public void Remove(IInputStateChangeMonitor monitor, long monitorIndex, bool deferRemoval) | |
| { | |
| if (listeners == null) | |
| return; | |
| for (var i = 0; i < signalled.length; ++i) | |
| if (ReferenceEquals(listeners[i].monitor, monitor) && listeners[i].monitorIndex == monitorIndex) | |
| { | |
| if (deferRemoval) | |
| { | |
| listeners[i] = default; | |
| memoryRegions[i] = default; | |
| signalled.ClearBit(i); | |
| needToCompactArrays = true; | |
| } | |
| else | |
| { | |
| RemoveAt(i); | |
| } | |
| break; | |
| } | |
| } | |
| public void Clear() | |
| { | |
| // We don't actually release memory we've potentially allocated but rather just reset | |
| // our count to zero. | |
| listeners.Clear(count); | |
| signalled.SetLength(0); | |
| needToCompactArrays = false; | |
| } | |
| public void CompactArrays() | |
| { | |
| for (var i = count - 1; i >= 0; --i) | |
| { | |
| var memoryRegion = memoryRegions[i]; | |
| if (memoryRegion.sizeInBits != 0) | |
| continue; | |
| RemoveAt(i); | |
| } | |
| needToCompactArrays = false; | |
| } | |
| private void RemoveAt(int i) | |
| { | |
| var numListeners = count; | |
| var numMemoryRegions = count; | |
| listeners.EraseAtWithCapacity(ref numListeners, i); | |
| memoryRegions.EraseAtWithCapacity(ref numMemoryRegions, i); | |
| signalled.SetLength(count - 1); | |
| } | |
| public void SortMonitorsByIndex() | |
| { | |
| // Insertion sort. | |
| for (var i = 1; i < signalled.length; ++i) | |
| { | |
| for (var j = i; j > 0; --j) | |
| { | |
| // Sort by complexities only to keep the sort stable | |
| // i.e. don't reverse the order of controls which have the same complexity | |
| var firstComplexity = InputActionState.GetComplexityFromMonitorIndex(listeners[j - 1].monitorIndex); | |
| var secondComplexity = InputActionState.GetComplexityFromMonitorIndex(listeners[j].monitorIndex); | |
| if (firstComplexity >= secondComplexity) | |
| break; | |
| listeners.SwapElements(j, j - 1); | |
| memoryRegions.SwapElements(j, j - 1); | |
| // We can ignore the `signalled` array here as we call this method only | |
| // when all monitors are in non-signalled state. | |
| } | |
| } | |
| needToUpdateOrderingOfMonitors = false; | |
| } | |
| } | |
| // NOTE: 'newState' can be a subset of the full state stored at 'oldState'. In this case, | |
| // 'newStateOffsetInBytes' must give the offset into the full state and 'newStateSizeInBytes' must | |
| // give the size of memory slice to be updated. | |
| private unsafe bool ProcessStateChangeMonitors(int deviceIndex, void* newStateFromEvent, void* oldStateOfDevice, uint newStateSizeInBytes, uint newStateOffsetInBytes) | |
| { | |
| if (m_StateChangeMonitors == null) | |
| return false; | |
| // We resize the monitor arrays only when someone adds to them so they | |
| // may be out of sync with the size of m_Devices. | |
| if (deviceIndex >= m_StateChangeMonitors.Length) | |
| return false; | |
| var memoryRegions = m_StateChangeMonitors[deviceIndex].memoryRegions; | |
| if (memoryRegions == null) | |
| return false; // No one cares about state changes on this device. | |
| var numMonitors = m_StateChangeMonitors[deviceIndex].count; | |
| var signalled = false; | |
| var signals = m_StateChangeMonitors[deviceIndex].signalled; | |
| var haveChangedSignalsBitfield = false; | |
| // For every memory region that overlaps what we got in the event, compare memory contents | |
| // between the old device state and what's in the event. If the contents different, the | |
| // respective state monitor signals. | |
| var newEventMemoryRegion = new MemoryHelpers.BitRegion(newStateOffsetInBytes, 0, newStateSizeInBytes * 8); | |
| for (var i = 0; i < numMonitors; ++i) | |
| { | |
| var memoryRegion = memoryRegions[i]; | |
| // Check if the monitor record has been wiped in the meantime. If so, remove it. | |
| if (memoryRegion.sizeInBits == 0) | |
| { | |
| ////REVIEW: Do we really care? It is nice that it's predictable this way but hardly a hard requirement | |
| // NOTE: We're using EraseAtWithCapacity here rather than EraseAtByMovingTail to preserve | |
| // order which makes the order of callbacks somewhat more predictable. | |
| var listenerCount = numMonitors; | |
| var memoryRegionCount = numMonitors; | |
| m_StateChangeMonitors[deviceIndex].listeners.EraseAtWithCapacity(ref listenerCount, i); | |
| memoryRegions.EraseAtWithCapacity(ref memoryRegionCount, i); | |
| signals.SetLength(numMonitors - 1); | |
| haveChangedSignalsBitfield = true; | |
| --numMonitors; | |
| --i; | |
| continue; | |
| } | |
| var overlap = newEventMemoryRegion.Overlap(memoryRegion); | |
| if (overlap.isEmpty || MemoryHelpers.Compare(oldStateOfDevice, (byte*)newStateFromEvent - newStateOffsetInBytes, overlap)) | |
| continue; | |
| signals.SetBit(i); | |
| haveChangedSignalsBitfield = true; | |
| signalled = true; | |
| } | |
| if (haveChangedSignalsBitfield) | |
| m_StateChangeMonitors[deviceIndex].signalled = signals; | |
| m_StateChangeMonitors[deviceIndex].needToCompactArrays = false; | |
| return signalled; | |
| } | |
| internal unsafe void FireStateChangeNotifications(int deviceIndex, double internalTime, InputEvent* eventPtr) | |
| { | |
| Debug.Assert(m_StateChangeMonitors != null); | |
| Debug.Assert(m_StateChangeMonitors.Length > deviceIndex); | |
| // NOTE: This method must be safe for mutating the state change monitor arrays from *within* | |
| // NotifyControlStateChanged()! This includes all monitors for the device being wiped | |
| // completely or arbitrary additions and removals having occurred. | |
| ref var signals = ref m_StateChangeMonitors[deviceIndex].signalled; | |
| ref var listeners = ref m_StateChangeMonitors[deviceIndex].listeners; | |
| var time = internalTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup; | |
| // If we don't have an event, gives us as dummy, invalid instance. | |
| // What matters is that InputEventPtr.valid is false for these. | |
| var tempEvent = new InputEvent(new FourCC('F', 'A', 'K', 'E'), InputEvent.kBaseEventSize, -1, internalTime); | |
| if (eventPtr == null) | |
| eventPtr = (InputEvent*)UnsafeUtility.AddressOf(ref tempEvent); | |
| // Call IStateChangeMonitor.NotifyControlStateChange for every monitor that is in | |
| // signalled state. | |
| eventPtr->handled = false; | |
| for (var i = 0; i < signals.length; ++i) | |
| { | |
| if (!signals.TestBit(i)) | |
| continue; | |
| var listener = listeners[i]; | |
| try | |
| { | |
| listener.monitor.NotifyControlStateChanged(listener.control, time, eventPtr, | |
| listener.monitorIndex); | |
| } | |
| catch (Exception exception) | |
| { | |
| Debug.LogError( | |
| $"Exception '{exception.GetType().Name}' thrown from state change monitor '{listener.monitor.GetType().Name}' on '{listener.control}'"); | |
| Debug.LogException(exception); | |
| } | |
| // If the monitor signalled that it has processed the state change, reset all signalled | |
| // state monitors in the same group. This is what causes "SHIFT+B" to prevent "B" from | |
| // also triggering. | |
| if (eventPtr->handled) | |
| { | |
| var groupIndex = listeners[i].groupIndex; | |
| for (var n = i + 1; n < signals.length; ++n) | |
| { | |
| // NOTE: We restrict the preemption logic here to a single monitor. Otherwise, | |
| // we will have to require that group indices are stable *between* | |
| // monitors. Two separate InputActionStates, for example, would have to | |
| // agree on group indices that valid *between* the two states or we end | |
| // up preempting unrelated inputs. | |
| // | |
| // Note that this implies there there is *NO* preemption between singleton | |
| // InputActions. This isn't intuitive. | |
| if (listeners[n].groupIndex == groupIndex && listeners[n].monitor == listener.monitor) | |
| signals.ClearBit(n); | |
| } | |
| // Need to reset it back to false as we may have more signalled state monitors that | |
| // aren't in the same group (i.e. have independent inputs). | |
| eventPtr->handled = false; | |
| } | |
| signals.ClearBit(i); | |
| } | |
| } | |
| private void ProcessStateChangeMonitorTimeouts() | |
| { | |
| if (m_StateChangeMonitorTimeouts.length == 0) | |
| return; | |
| // Go through the list and both trigger expired timers and remove any irrelevant | |
| // ones by compacting the array. | |
| // NOTE: We do not actually release any memory we may have allocated. | |
| var currentTime = m_Runtime.currentTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup; | |
| var remainingTimeoutCount = 0; | |
| for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i) | |
| { | |
| // If we have reset this entry in RemoveStateChangeMonitorTimeouts(), | |
| // skip over it and let compaction get rid of it. | |
| if (m_StateChangeMonitorTimeouts[i].control == null) | |
| continue; | |
| var timerExpirationTime = m_StateChangeMonitorTimeouts[i].time; | |
| if (timerExpirationTime <= currentTime) | |
| { | |
| var timeout = m_StateChangeMonitorTimeouts[i]; | |
| timeout.monitor.NotifyTimerExpired(timeout.control, | |
| currentTime, timeout.monitorIndex, timeout.timerIndex); | |
| // Compaction will get rid of the entry. | |
| } | |
| else | |
| { | |
| // Rather than repeatedly calling RemoveAt() and thus potentially | |
| // moving the same data over and over again, we compact the array | |
| // on the fly and move entries in the array down as needed. | |
| if (i != remainingTimeoutCount) | |
| m_StateChangeMonitorTimeouts[remainingTimeoutCount] = m_StateChangeMonitorTimeouts[i]; | |
| ++remainingTimeoutCount; | |
| } | |
| } | |
| m_StateChangeMonitorTimeouts.SetLength(remainingTimeoutCount); | |
| } | |
| } | |
| } | |