123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- 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)
- {
- if (m_DevicesCount <= 0) return;
-
- 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);
- }
- }
- }
|