A game about forced loneliness, made by TACStudios
1using System;
2using Unity.Collections.LowLevel.Unsafe;
3using UnityEngine.InputSystem.LowLevel;
4using UnityEngine.InputSystem.Utilities;
5
6namespace UnityEngine.InputSystem
7{
8 internal partial class InputManager
9 {
10 // Indices correspond with those in m_Devices.
11 internal StateChangeMonitorsForDevice[] m_StateChangeMonitors;
12 private InlinedArray<StateChangeMonitorTimeout> m_StateChangeMonitorTimeouts;
13
14 ////TODO: support combining monitors for bitfields
15 public void AddStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex, uint groupIndex)
16 {
17 if (m_DevicesCount <= 0) return;
18
19 var device = control.device;
20 var deviceIndex = device.m_DeviceIndex;
21 Debug.Assert(deviceIndex != InputDevice.kInvalidDeviceIndex);
22
23 // Allocate/reallocate monitor arrays, if necessary.
24 // We lazy-sync it to array of devices.
25 if (m_StateChangeMonitors == null)
26 m_StateChangeMonitors = new StateChangeMonitorsForDevice[m_DevicesCount];
27 else if (m_StateChangeMonitors.Length <= deviceIndex)
28 Array.Resize(ref m_StateChangeMonitors, m_DevicesCount);
29
30 // If we have removed monitors
31 if (!isProcessingEvents && m_StateChangeMonitors[deviceIndex].needToCompactArrays)
32 m_StateChangeMonitors[deviceIndex].CompactArrays();
33
34 // Add record.
35 m_StateChangeMonitors[deviceIndex].Add(control, monitor, monitorIndex, groupIndex);
36 }
37
38 private void RemoveStateChangeMonitors(InputDevice device)
39 {
40 if (m_StateChangeMonitors == null)
41 return;
42
43 var deviceIndex = device.m_DeviceIndex;
44 Debug.Assert(deviceIndex != InputDevice.kInvalidDeviceIndex);
45
46 if (deviceIndex >= m_StateChangeMonitors.Length)
47 return;
48
49 m_StateChangeMonitors[deviceIndex].Clear();
50
51 // Clear timeouts pending on any control on the device.
52 for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i)
53 if (m_StateChangeMonitorTimeouts[i].control?.device == device)
54 m_StateChangeMonitorTimeouts[i] = default;
55 }
56
57 public void RemoveStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex)
58 {
59 if (m_StateChangeMonitors == null)
60 return;
61
62 var device = control.device;
63 var deviceIndex = device.m_DeviceIndex;
64
65 // Ignore if device has already been removed.
66 if (deviceIndex == InputDevice.kInvalidDeviceIndex)
67 return;
68
69 // Ignore if there are no state monitors set up for the device.
70 if (deviceIndex >= m_StateChangeMonitors.Length)
71 return;
72
73 m_StateChangeMonitors[deviceIndex].Remove(monitor, monitorIndex, isProcessingEvents);
74
75 // Remove pending timeouts on the monitor.
76 for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i)
77 if (m_StateChangeMonitorTimeouts[i].monitor == monitor &&
78 m_StateChangeMonitorTimeouts[i].monitorIndex == monitorIndex)
79 m_StateChangeMonitorTimeouts[i] = default;
80 }
81
82 public void AddStateChangeMonitorTimeout(InputControl control, IInputStateChangeMonitor monitor, double time, long monitorIndex, int timerIndex)
83 {
84 m_StateChangeMonitorTimeouts.Append(
85 new StateChangeMonitorTimeout
86 {
87 control = control,
88 time = time,
89 monitor = monitor,
90 monitorIndex = monitorIndex,
91 timerIndex = timerIndex,
92 });
93 }
94
95 public void RemoveStateChangeMonitorTimeout(IInputStateChangeMonitor monitor, long monitorIndex, int timerIndex)
96 {
97 var timeoutCount = m_StateChangeMonitorTimeouts.length;
98 for (var i = 0; i < timeoutCount; ++i)
99 {
100 ////REVIEW: can we avoid the repeated array lookups without copying the struct out?
101 if (ReferenceEquals(m_StateChangeMonitorTimeouts[i].monitor, monitor)
102 && m_StateChangeMonitorTimeouts[i].monitorIndex == monitorIndex
103 && m_StateChangeMonitorTimeouts[i].timerIndex == timerIndex)
104 {
105 m_StateChangeMonitorTimeouts[i] = default;
106 break;
107 }
108 }
109 }
110
111 private void SortStateChangeMonitorsIfNecessary(int deviceIndex)
112 {
113 if (m_StateChangeMonitors != null && deviceIndex < m_StateChangeMonitors.Length &&
114 m_StateChangeMonitors[deviceIndex].needToUpdateOrderingOfMonitors)
115 m_StateChangeMonitors[deviceIndex].SortMonitorsByIndex();
116 }
117
118 public void SignalStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor)
119 {
120 var device = control.device;
121 var deviceIndex = device.m_DeviceIndex;
122
123 ref var monitorsForDevice = ref m_StateChangeMonitors[deviceIndex];
124 for (var i = 0; i < monitorsForDevice.signalled.length; ++i)
125 {
126 SortStateChangeMonitorsIfNecessary(i);
127
128 ref var listener = ref monitorsForDevice.listeners[i];
129 if (listener.control == control && listener.monitor == monitor)
130 monitorsForDevice.signalled.SetBit(i);
131 }
132 }
133
134 public unsafe void FireStateChangeNotifications()
135 {
136 var time = m_Runtime.currentTime;
137 var count = Math.Min(m_StateChangeMonitors.LengthSafe(), m_DevicesCount);
138 for (var i = 0; i < count; ++i)
139 FireStateChangeNotifications(i, time, null);
140 }
141
142 // Record for a timeout installed on a state change monitor.
143 private struct StateChangeMonitorTimeout
144 {
145 public InputControl control;
146 public double time;
147 public IInputStateChangeMonitor monitor;
148 public long monitorIndex;
149 public int timerIndex;
150 }
151
152 // Maps a single control to an action interested in the control. If
153 // multiple actions are interested in the same control, we will end up
154 // processing the control repeatedly but we assume this is the exception
155 // and so optimize for the case where there's only one action going to
156 // a control.
157 //
158 // Split into two structures to keep data needed only when there is an
159 // actual value change out of the data we need for doing the scanning.
160 internal struct StateChangeMonitorListener
161 {
162 public InputControl control;
163 public IInputStateChangeMonitor monitor;
164 public long monitorIndex;
165 public uint groupIndex;
166 }
167
168 internal struct StateChangeMonitorsForDevice
169 {
170 public MemoryHelpers.BitRegion[] memoryRegions;
171 public StateChangeMonitorListener[] listeners;
172 public DynamicBitfield signalled;
173 public bool needToUpdateOrderingOfMonitors;
174 public bool needToCompactArrays;
175
176 public int count => signalled.length;
177
178 public void Add(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex, uint groupIndex)
179 {
180 // NOTE: This method must only *append* to arrays. This way we can safely add data while traversing
181 // the arrays in FireStateChangeNotifications. Note that appending *may* mean that the arrays
182 // are switched to larger arrays.
183
184 // Record listener.
185 var listenerCount = signalled.length;
186 ArrayHelpers.AppendWithCapacity(ref listeners, ref listenerCount,
187 new StateChangeMonitorListener
188 { monitor = monitor, monitorIndex = monitorIndex, groupIndex = groupIndex, control = control });
189
190 // Record memory region.
191 ref var controlStateBlock = ref control.m_StateBlock;
192 var memoryRegionCount = signalled.length;
193 ArrayHelpers.AppendWithCapacity(ref memoryRegions, ref memoryRegionCount,
194 new MemoryHelpers.BitRegion(controlStateBlock.byteOffset - control.device.stateBlock.byteOffset,
195 controlStateBlock.bitOffset, controlStateBlock.sizeInBits));
196
197 signalled.SetLength(signalled.length + 1);
198
199 needToUpdateOrderingOfMonitors = true;
200 }
201
202 public void Remove(IInputStateChangeMonitor monitor, long monitorIndex, bool deferRemoval)
203 {
204 if (listeners == null)
205 return;
206
207 for (var i = 0; i < signalled.length; ++i)
208 if (ReferenceEquals(listeners[i].monitor, monitor) && listeners[i].monitorIndex == monitorIndex)
209 {
210 if (deferRemoval)
211 {
212 listeners[i] = default;
213 memoryRegions[i] = default;
214 signalled.ClearBit(i);
215 needToCompactArrays = true;
216 }
217 else
218 {
219 RemoveAt(i);
220 }
221
222 break;
223 }
224 }
225
226 public void Clear()
227 {
228 // We don't actually release memory we've potentially allocated but rather just reset
229 // our count to zero.
230 listeners.Clear(count);
231 signalled.SetLength(0);
232
233 needToCompactArrays = false;
234 }
235
236 public void CompactArrays()
237 {
238 for (var i = count - 1; i >= 0; --i)
239 {
240 var memoryRegion = memoryRegions[i];
241 if (memoryRegion.sizeInBits != 0)
242 continue;
243
244 RemoveAt(i);
245 }
246
247 needToCompactArrays = false;
248 }
249
250 private void RemoveAt(int i)
251 {
252 var numListeners = count;
253 var numMemoryRegions = count;
254 listeners.EraseAtWithCapacity(ref numListeners, i);
255 memoryRegions.EraseAtWithCapacity(ref numMemoryRegions, i);
256 signalled.SetLength(count - 1);
257 }
258
259 public void SortMonitorsByIndex()
260 {
261 // Insertion sort.
262 for (var i = 1; i < signalled.length; ++i)
263 {
264 for (var j = i; j > 0; --j)
265 {
266 // Sort by complexities only to keep the sort stable
267 // i.e. don't reverse the order of controls which have the same complexity
268 var firstComplexity = InputActionState.GetComplexityFromMonitorIndex(listeners[j - 1].monitorIndex);
269 var secondComplexity = InputActionState.GetComplexityFromMonitorIndex(listeners[j].monitorIndex);
270 if (firstComplexity >= secondComplexity)
271 break;
272
273 listeners.SwapElements(j, j - 1);
274 memoryRegions.SwapElements(j, j - 1);
275
276 // We can ignore the `signalled` array here as we call this method only
277 // when all monitors are in non-signalled state.
278 }
279 }
280
281 needToUpdateOrderingOfMonitors = false;
282 }
283 }
284
285 // NOTE: 'newState' can be a subset of the full state stored at 'oldState'. In this case,
286 // 'newStateOffsetInBytes' must give the offset into the full state and 'newStateSizeInBytes' must
287 // give the size of memory slice to be updated.
288 private unsafe bool ProcessStateChangeMonitors(int deviceIndex, void* newStateFromEvent, void* oldStateOfDevice, uint newStateSizeInBytes, uint newStateOffsetInBytes)
289 {
290 if (m_StateChangeMonitors == null)
291 return false;
292
293 // We resize the monitor arrays only when someone adds to them so they
294 // may be out of sync with the size of m_Devices.
295 if (deviceIndex >= m_StateChangeMonitors.Length)
296 return false;
297
298 var memoryRegions = m_StateChangeMonitors[deviceIndex].memoryRegions;
299 if (memoryRegions == null)
300 return false; // No one cares about state changes on this device.
301
302 var numMonitors = m_StateChangeMonitors[deviceIndex].count;
303 var signalled = false;
304 var signals = m_StateChangeMonitors[deviceIndex].signalled;
305 var haveChangedSignalsBitfield = false;
306
307 // For every memory region that overlaps what we got in the event, compare memory contents
308 // between the old device state and what's in the event. If the contents different, the
309 // respective state monitor signals.
310 var newEventMemoryRegion = new MemoryHelpers.BitRegion(newStateOffsetInBytes, 0, newStateSizeInBytes * 8);
311 for (var i = 0; i < numMonitors; ++i)
312 {
313 var memoryRegion = memoryRegions[i];
314
315 // Check if the monitor record has been wiped in the meantime. If so, remove it.
316 if (memoryRegion.sizeInBits == 0)
317 {
318 ////REVIEW: Do we really care? It is nice that it's predictable this way but hardly a hard requirement
319 // NOTE: We're using EraseAtWithCapacity here rather than EraseAtByMovingTail to preserve
320 // order which makes the order of callbacks somewhat more predictable.
321
322 var listenerCount = numMonitors;
323 var memoryRegionCount = numMonitors;
324 m_StateChangeMonitors[deviceIndex].listeners.EraseAtWithCapacity(ref listenerCount, i);
325 memoryRegions.EraseAtWithCapacity(ref memoryRegionCount, i);
326 signals.SetLength(numMonitors - 1);
327 haveChangedSignalsBitfield = true;
328 --numMonitors;
329 --i;
330 continue;
331 }
332
333 var overlap = newEventMemoryRegion.Overlap(memoryRegion);
334 if (overlap.isEmpty || MemoryHelpers.Compare(oldStateOfDevice, (byte*)newStateFromEvent - newStateOffsetInBytes, overlap))
335 continue;
336
337 signals.SetBit(i);
338 haveChangedSignalsBitfield = true;
339 signalled = true;
340 }
341
342 if (haveChangedSignalsBitfield)
343 m_StateChangeMonitors[deviceIndex].signalled = signals;
344
345 m_StateChangeMonitors[deviceIndex].needToCompactArrays = false;
346
347 return signalled;
348 }
349
350 internal unsafe void FireStateChangeNotifications(int deviceIndex, double internalTime, InputEvent* eventPtr)
351 {
352 if (m_StateChangeMonitors == null)
353 {
354 Debug.Assert(false, "m_StateChangeMonitors is null - has AddStateChangeMonitor been called?");
355 return;
356 }
357 if (m_StateChangeMonitors.Length <= deviceIndex)
358 {
359 Debug.Assert(false, $"deviceIndex {deviceIndex} passed to FireStateChangeNotifications is out of bounds (current length {m_StateChangeMonitors.Length}).");
360 return;
361 }
362
363 // NOTE: This method must be safe for mutating the state change monitor arrays from *within*
364 // NotifyControlStateChanged()! This includes all monitors for the device being wiped
365 // completely or arbitrary additions and removals having occurred.
366
367 ref var signals = ref m_StateChangeMonitors[deviceIndex].signalled;
368 if (signals.AnyBitIsSet() && m_StateChangeMonitors[deviceIndex].listeners == null)
369 {
370 Debug.Assert(false, $"A state change for device {deviceIndex} has been set, but list of listeners is null.");
371 return;
372 }
373
374 ref var listeners = ref m_StateChangeMonitors[deviceIndex].listeners;
375 var time = internalTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;
376
377 // If we don't have an event, gives us as dummy, invalid instance.
378 // What matters is that InputEventPtr.valid is false for these.
379 var tempEvent = new InputEvent(new FourCC('F', 'A', 'K', 'E'), InputEvent.kBaseEventSize, -1, internalTime);
380 if (eventPtr == null)
381 eventPtr = (InputEvent*)UnsafeUtility.AddressOf(ref tempEvent);
382
383 // Call IStateChangeMonitor.NotifyControlStateChange for every monitor that is in
384 // signalled state.
385 eventPtr->handled = false;
386 for (var i = 0; i < signals.length; ++i)
387 {
388 if (!signals.TestBit(i))
389 continue;
390
391 var listener = listeners[i];
392 try
393 {
394 listener.monitor.NotifyControlStateChanged(listener.control, time, eventPtr,
395 listener.monitorIndex);
396 }
397 catch (Exception exception)
398 {
399 Debug.LogError(
400 $"Exception '{exception.GetType().Name}' thrown from state change monitor '{listener.monitor.GetType().Name}' on '{listener.control}'");
401 Debug.LogException(exception);
402 }
403
404 // If the monitor signalled that it has processed the state change, reset all signalled
405 // state monitors in the same group. This is what causes "SHIFT+B" to prevent "B" from
406 // also triggering.
407 if (eventPtr->handled)
408 {
409 var groupIndex = listeners[i].groupIndex;
410 for (var n = i + 1; n < signals.length; ++n)
411 {
412 // NOTE: We restrict the preemption logic here to a single monitor. Otherwise,
413 // we will have to require that group indices are stable *between*
414 // monitors. Two separate InputActionStates, for example, would have to
415 // agree on group indices that valid *between* the two states or we end
416 // up preempting unrelated inputs.
417 //
418 // Note that this implies there there is *NO* preemption between singleton
419 // InputActions. This isn't intuitive.
420 if (listeners[n].groupIndex == groupIndex && listeners[n].monitor == listener.monitor)
421 signals.ClearBit(n);
422 }
423
424 // Need to reset it back to false as we may have more signalled state monitors that
425 // aren't in the same group (i.e. have independent inputs).
426 eventPtr->handled = false;
427 }
428
429 signals.ClearBit(i);
430 }
431 }
432
433 private void ProcessStateChangeMonitorTimeouts()
434 {
435 if (m_StateChangeMonitorTimeouts.length == 0)
436 return;
437
438 // Go through the list and both trigger expired timers and remove any irrelevant
439 // ones by compacting the array.
440 // NOTE: We do not actually release any memory we may have allocated.
441 var currentTime = m_Runtime.currentTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;
442 var remainingTimeoutCount = 0;
443 for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i)
444 {
445 // If we have reset this entry in RemoveStateChangeMonitorTimeouts(),
446 // skip over it and let compaction get rid of it.
447 if (m_StateChangeMonitorTimeouts[i].control == null)
448 continue;
449
450 var timerExpirationTime = m_StateChangeMonitorTimeouts[i].time;
451 if (timerExpirationTime <= currentTime)
452 {
453 var timeout = m_StateChangeMonitorTimeouts[i];
454 timeout.monitor.NotifyTimerExpired(timeout.control,
455 currentTime, timeout.monitorIndex, timeout.timerIndex);
456
457 // Compaction will get rid of the entry.
458 }
459 else
460 {
461 // Rather than repeatedly calling RemoveAt() and thus potentially
462 // moving the same data over and over again, we compact the array
463 // on the fly and move entries in the array down as needed.
464 if (i != remainingTimeoutCount)
465 m_StateChangeMonitorTimeouts[remainingTimeoutCount] = m_StateChangeMonitorTimeouts[i];
466 ++remainingTimeoutCount;
467 }
468 }
469
470 m_StateChangeMonitorTimeouts.SetLength(remainingTimeoutCount);
471 }
472 }
473}