using System; using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine.InputSystem.Utilities; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.InputSystem.Layouts; using UnityEngine.Profiling; namespace UnityEngine.InputSystem.LowLevel { /// /// InputEventTrace lets you record input events for later processing. It also has features for writing traces /// to disk, for loading them from disk, and for playing back previously recorded traces. /// /// /// InputEventTrace lets you record input events into a buffer for either a specific device, or for all events /// received by the input system. This is useful for testing purposes or for replaying recorded input. /// /// Note that event traces must be disposed of (by calling ) after use or they /// will leak memory on the unmanaged (C++) memory heap. /// /// Event traces are serializable such that they can survive domain reloads in the editor. /// [Serializable] public sealed unsafe class InputEventTrace : IDisposable, IEnumerable { private const int kDefaultBufferSize = 1024 * 1024; /// /// If is enabled, an with this /// code in its is recorded whenever the input system starts a new update, i.e. /// whenever is triggered. This is useful for replaying events in such /// a way that they are correctly spaced out over frames. /// public static FourCC FrameMarkerEvent => new FourCC('F', 'R', 'M', 'E'); /// /// Set device to record events for. Set to by default /// in which case events from all devices are recorded. /// public int deviceId { get => m_DeviceId; set => m_DeviceId = value; } /// /// Whether the trace is currently recording input. /// /// True if the trace is currently recording events. /// /// public bool enabled => m_Enabled; /// /// If true, input update boundaries will be recorded as events. By default, this is off. /// /// Whether frame boundaries should be recorded in the trace. /// /// When recording with this off, all events are written one after the other for as long /// as the recording is active. This means that when a recording runs over multiple frames, /// it is no longer possible for the trace to tell which events happened in distinct frames. /// /// By turning this feature on, frame marker events (i.e. instances /// with set to ) will be written /// to the trace every time an input update occurs. When playing such a trace back via , events will get spaced out over frames corresponding /// to how they were spaced out when input was initially recorded. /// /// Note that having this feature enabled will fill up traces much quicker. Instead of being /// filled up only when there is input, TODO /// /// /// public bool recordFrameMarkers { get => m_RecordFrameMarkers; set { if (m_RecordFrameMarkers == value) return; m_RecordFrameMarkers = value; if (m_Enabled) { if (value) InputSystem.onBeforeUpdate += OnBeforeUpdate; else InputSystem.onBeforeUpdate -= OnBeforeUpdate; } } } /// /// Total number of events currently in the trace. /// /// Number of events recorded in the trace. public long eventCount => m_EventCount; /// /// The amount of memory consumed by all events combined that are currently /// stored in the trace. /// /// Total size of event data currently in trace. public long totalEventSizeInBytes => m_EventSizeInBytes; /// /// Total size of memory buffer (in bytes) currently allocated. /// /// Size of memory currently allocated. /// /// The buffer is allocated on the unmanaged heap. /// public long allocatedSizeInBytes => m_EventBuffer != default ? m_EventBufferSize : 0; /// /// Largest size (in bytes) that the memory buffer is allowed to grow to. By default, this is /// the same as meaning that the buffer is not allowed to grow but will /// rather wrap around when full. /// /// Largest size the memory buffer is allowed to grow to. public long maxSizeInBytes => m_MaxEventBufferSize; /// /// Information about all devices for which events have been recorded in the trace. /// /// Record of devices recorded in the trace. public ReadOnlyArray deviceInfos => m_DeviceInfos; /// /// Optional delegate to decide whether an input should be stored in a trace. Null by default. /// /// Delegate to accept or reject individual events. /// /// When this is set, the callback will be invoked on every event that would otherwise be stored /// directly in the trace. If the callback returns true, the trace will continue to record /// the event. If the callback returns false, the event will be ignored and not recorded. /// /// The callback should generally mutate the event. If you do so, note that this will impact /// event processing in general, not just recording of the event in the trace. /// public Func onFilterEvent { get => m_OnFilterEvent; set => m_OnFilterEvent = value; } /// /// Event that is triggered every time an event has been recorded in the trace. /// public event Action onEvent { add => m_EventListeners.AddCallback(value); remove => m_EventListeners.RemoveCallback(value); } public InputEventTrace(InputDevice device, long bufferSizeInBytes = kDefaultBufferSize, bool growBuffer = false, long maxBufferSizeInBytes = -1, long growIncrementSizeInBytes = -1) : this(bufferSizeInBytes, growBuffer, maxBufferSizeInBytes, growIncrementSizeInBytes) { if (device == null) throw new ArgumentNullException(nameof(device)); m_DeviceId = device.deviceId; } /// /// Create a disabled event trace that does not perform any allocation yet. An event trace only starts consuming resources /// the first time it is enabled. /// /// Size of buffer that will be allocated on first event captured by trace. Defaults to 1MB. /// If true, the event buffer will be grown automatically when it reaches capacity, up to a maximum /// size of . This is off by default. /// If is true, this is the maximum size that the buffer should /// be grown to. If the maximum size is reached, old events are being overwritten. public InputEventTrace(long bufferSizeInBytes = kDefaultBufferSize, bool growBuffer = false, long maxBufferSizeInBytes = -1, long growIncrementSizeInBytes = -1) { m_EventBufferSize = (uint)bufferSizeInBytes; if (growBuffer) { if (maxBufferSizeInBytes < 0) m_MaxEventBufferSize = 256 * kDefaultBufferSize; else m_MaxEventBufferSize = maxBufferSizeInBytes; if (growIncrementSizeInBytes < 0) m_GrowIncrementSize = kDefaultBufferSize; else m_GrowIncrementSize = growIncrementSizeInBytes; } else { m_MaxEventBufferSize = m_EventBufferSize; } } /// /// Write the contents of the event trace to a file. /// /// Path of the file to write. /// is null or empty. /// is invalid. /// A directory in is invalid. /// cannot be accessed. /// public void WriteTo(string filePath) { if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); using (var stream = File.OpenWrite(filePath)) WriteTo(stream); } /// /// Write the contents of the event trace to the given stream. /// /// Stream to write the data to. Must support seeking (i.e. Stream.canSeek must be true). /// is null. /// does not support seeking. /// An error occurred trying to write to . public void WriteTo(Stream stream) { if (stream == null) throw new ArgumentNullException(nameof(stream)); if (!stream.CanSeek) throw new ArgumentException("Stream does not support seeking", nameof(stream)); var writer = new BinaryWriter(stream); var flags = default(FileFlags); if (InputSystem.settings.updateMode == InputSettings.UpdateMode.ProcessEventsInFixedUpdate) flags |= FileFlags.FixedUpdate; // Write header. writer.Write(kFileFormat); writer.Write(kFileVersion); writer.Write((int)flags); writer.Write((int)Application.platform); writer.Write((ulong)m_EventCount); writer.Write((ulong)m_EventSizeInBytes); // Write events. foreach (var eventPtr in this) { ////TODO: find way to directly write a byte* buffer to the stream instead of copying to a temp byte[] var sizeInBytes = eventPtr.sizeInBytes; var buffer = new byte[sizeInBytes]; fixed(byte* bufferPtr = buffer) { UnsafeUtility.MemCpy(bufferPtr, eventPtr.data, sizeInBytes); writer.Write(buffer); } } // Write devices. writer.Flush(); var positionOfDeviceList = stream.Position; var deviceCount = m_DeviceInfos.LengthSafe(); writer.Write(deviceCount); for (var i = 0; i < deviceCount; ++i) { ref var device = ref m_DeviceInfos[i]; writer.Write(device.deviceId); writer.Write(device.layout); writer.Write(device.stateFormat); writer.Write(device.stateSizeInBytes); writer.Write(device.m_FullLayoutJson ?? string.Empty); } // Write offset of device list. writer.Flush(); var offsetOfDeviceList = stream.Position - positionOfDeviceList; writer.Write(offsetOfDeviceList); } /// /// Read the contents of an input event trace stored in the given file. /// /// Path to a file. /// is null or empty. /// is invalid. /// A directory in is invalid. /// cannot be accessed. /// /// This method replaces the contents of the trace with those read from the given file. /// /// public void ReadFrom(string filePath) { if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); using (var stream = File.OpenRead(filePath)) ReadFrom(stream); } /// /// Read the contents of an input event trace from the given stream. /// /// A stream of binary data containing a recorded event trace as written out with . /// Must support reading. /// is null. /// does not support reading. /// An error occurred trying to read from . /// /// This method replaces the contents of the event trace with those read from the stream. It does not append /// to the existing trace. /// /// public void ReadFrom(Stream stream) { if (stream == null) throw new ArgumentNullException(nameof(stream)); if (!stream.CanRead) throw new ArgumentException("Stream does not support reading", nameof(stream)); var reader = new BinaryReader(stream); // Read header. if (reader.ReadInt32() != kFileFormat) throw new IOException($"Stream does not appear to be an InputEventTrace (no '{kFileFormat}' code)"); if (reader.ReadInt32() > kFileVersion) throw new IOException($"Stream is an InputEventTrace but a newer version (expected version {kFileVersion} or below)"); reader.ReadInt32(); // Flags; ignored for now. reader.ReadInt32(); // Platform; for now we're not doing anything with it. var eventCount = reader.ReadUInt64(); var totalEventSizeInBytes = reader.ReadUInt64(); var oldBuffer = m_EventBuffer; if (eventCount > 0 && totalEventSizeInBytes > 0) { // Allocate buffer, if need be. byte* buffer; if (m_EventBuffer != null && m_EventBufferSize >= (long)totalEventSizeInBytes) { // Existing buffer is large enough. buffer = m_EventBuffer; } else { buffer = (byte*)UnsafeUtility.Malloc((long)totalEventSizeInBytes, InputEvent.kAlignment, Allocator.Persistent); m_EventBufferSize = (long)totalEventSizeInBytes; } try { // Read events. var tailPtr = buffer; var endPtr = tailPtr + totalEventSizeInBytes; var totalEventSize = 0L; for (var i = 0ul; i < eventCount; ++i) { var eventType = reader.ReadInt32(); var eventSizeInBytes = (uint)reader.ReadUInt16(); var eventDeviceId = (uint)reader.ReadUInt16(); if (eventSizeInBytes > endPtr - tailPtr) break; *(int*)tailPtr = eventType; tailPtr += 4; *(ushort*)tailPtr = (ushort)eventSizeInBytes; tailPtr += 2; *(ushort*)tailPtr = (ushort)eventDeviceId; tailPtr += 2; ////TODO: find way to directly read from stream into a byte* pointer var remainingSize = (int)eventSizeInBytes - sizeof(int) - sizeof(short) - sizeof(short); var tempBuffer = reader.ReadBytes(remainingSize); fixed(byte* tempBufferPtr = tempBuffer) UnsafeUtility.MemCpy(tailPtr, tempBufferPtr, remainingSize); tailPtr += remainingSize.AlignToMultipleOf(InputEvent.kAlignment); totalEventSize += eventSizeInBytes.AlignToMultipleOf(InputEvent.kAlignment); if (tailPtr >= endPtr) break; } // Read device infos. var deviceCount = reader.ReadInt32(); var deviceInfos = new DeviceInfo[deviceCount]; for (var i = 0; i < deviceCount; ++i) { deviceInfos[i] = new DeviceInfo { deviceId = reader.ReadInt32(), layout = reader.ReadString(), stateFormat = reader.ReadInt32(), stateSizeInBytes = reader.ReadInt32(), m_FullLayoutJson = reader.ReadString() }; } // Install buffer. m_EventBuffer = buffer; m_EventBufferHead = m_EventBuffer; m_EventBufferTail = endPtr; m_EventCount = (long)eventCount; m_EventSizeInBytes = totalEventSize; m_DeviceInfos = deviceInfos; } catch { if (buffer != oldBuffer) UnsafeUtility.Free(buffer, Allocator.Persistent); throw; } } else { m_EventBuffer = default; m_EventBufferHead = default; m_EventBufferTail = default; } // Release old buffer, if we've switched to a new one. if (m_EventBuffer != oldBuffer && oldBuffer != null) UnsafeUtility.Free(oldBuffer, Allocator.Persistent); ++m_ChangeCounter; } /// /// Load an input event trace from the given file. /// /// Path to a file. /// is null or empty. /// is invalid. /// A directory in is invalid. /// cannot be accessed. /// /// public static InputEventTrace LoadFrom(string filePath) { if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); using (var stream = File.OpenRead(filePath)) return LoadFrom(stream); } /// /// Load an event trace from a previously captured event stream. /// /// A stream as written by . Must support reading. /// The loaded event trace. /// is not readable. /// is null. /// The stream cannot be loaded (e.g. wrong format; details in the exception). /// public static InputEventTrace LoadFrom(Stream stream) { if (stream == null) throw new ArgumentNullException(nameof(stream)); if (!stream.CanRead) throw new ArgumentException("Stream must be readable", nameof(stream)); var trace = new InputEventTrace(); trace.ReadFrom(stream); return trace; } /// /// Start a replay of the events in the trace. /// /// An object that controls playback. /// /// Calling this method implicitly turns off recording, if currently enabled (i.e. it calls ), /// as replaying an event trace cannot be done while it is also concurrently modified. /// public ReplayController Replay() { Disable(); return new ReplayController(this); } /// /// Resize the current event memory buffer to the specified size. /// /// Size to allocate for the buffer. /// Optional parameter to specifying the mark up to which the buffer is allowed to grow. By default, /// this is negative which indicates the buffer should not grow. In this case, will be set /// to . If this parameter is a non-negative number, it must be greater than or equal to /// and will become the new value for . /// True if the new buffer was successfully allocated. /// is negative. public bool Resize(long newBufferSize, long newMaxBufferSize = -1) { if (newBufferSize <= 0) throw new ArgumentException("Size must be positive", nameof(newBufferSize)); if (m_EventBufferSize == newBufferSize) return true; if (newMaxBufferSize < newBufferSize) newMaxBufferSize = newBufferSize; // Allocate. var newEventBuffer = (byte*)UnsafeUtility.Malloc(newBufferSize, InputEvent.kAlignment, Allocator.Persistent); if (newEventBuffer == default) return false; // If we have existing contents, migrate them. if (m_EventCount > 0) { // If we're shrinking the buffer or have a buffer that has already wrapped around, // migrate events one by one. if (newBufferSize < m_EventBufferSize || m_HasWrapped) { var fromPtr = new InputEventPtr((InputEvent*)m_EventBufferHead); var toPtr = (InputEvent*)newEventBuffer; var newEventCount = 0; var newEventSizeInBytes = 0; var remainingEventBytes = m_EventSizeInBytes; for (var i = 0; i < m_EventCount; ++i) { var eventSizeInBytes = fromPtr.sizeInBytes; var alignedEventSizeInBytes = eventSizeInBytes.AlignToMultipleOf(InputEvent.kAlignment); // We only start copying once we know that the remaining events we have fit in the new buffer. // This way we get the newest events and not the oldest ones. if (remainingEventBytes <= newBufferSize) { UnsafeUtility.MemCpy(toPtr, fromPtr.ToPointer(), eventSizeInBytes); toPtr = InputEvent.GetNextInMemory(toPtr); newEventSizeInBytes += (int)alignedEventSizeInBytes; ++newEventCount; } remainingEventBytes -= alignedEventSizeInBytes; if (!GetNextEvent(ref fromPtr)) break; } m_HasWrapped = false; m_EventCount = newEventCount; m_EventSizeInBytes = newEventSizeInBytes; } else { // Simple case of just having to copy everything between head and tail. UnsafeUtility.MemCpy(newEventBuffer, m_EventBufferHead, m_EventSizeInBytes); } } if (m_EventBuffer != null) UnsafeUtility.Free(m_EventBuffer, Allocator.Persistent); m_EventBufferSize = newBufferSize; m_EventBuffer = newEventBuffer; m_EventBufferHead = newEventBuffer; m_EventBufferTail = m_EventBuffer + m_EventSizeInBytes; m_MaxEventBufferSize = newMaxBufferSize; ++m_ChangeCounter; return true; } /// /// Reset the trace. Clears all recorded events. /// public void Clear() { m_EventBufferHead = m_EventBufferTail = default; m_EventCount = 0; m_EventSizeInBytes = 0; ++m_ChangeCounter; m_DeviceInfos = null; } /// /// Start recording events. /// /// public void Enable() { if (m_Enabled) return; if (m_EventBuffer == default) Allocate(); InputSystem.onEvent += OnInputEvent; if (m_RecordFrameMarkers) InputSystem.onBeforeUpdate += OnBeforeUpdate; m_Enabled = true; } /// /// Stop recording events. /// /// public void Disable() { if (!m_Enabled) return; InputSystem.onEvent -= OnInputEvent; InputSystem.onBeforeUpdate -= OnBeforeUpdate; m_Enabled = false; } /// /// Based on the given event pointer, return a pointer to the next event in the trace. /// /// A pointer to an event in the trace or a default(InputEventTrace). In the former case, /// the pointer will be updated to the next event, if there is one. In the latter case, the pointer will be updated /// to the first event in the trace, if there is one. /// True if current has been set to the next event, false otherwise. /// /// Event storage in memory may be circular if the event buffer is fixed in size or has reached maximum /// size and new events start overwriting old events. This method will automatically start with the first /// event when the given event is null. Any subsequent call with then loop over /// the remaining events until no more events are available. /// /// Note that it is VERY IMPORTANT that the buffer is not modified while iterating over events this way. /// If this is not ensured, invalid memory accesses may result. /// /// /// /// // Loop over all events in the InputEventTrace in the `trace` variable. /// var current = default(InputEventPtr); /// while (trace.GetNextEvent(ref current)) /// { /// Debug.Log(current); /// } /// /// /// public bool GetNextEvent(ref InputEventPtr current) { if (m_EventBuffer == default) return false; // If head is null, tail is too and it means there's nothing in the // buffer yet. if (m_EventBufferHead == default) return false; // If current is null, start iterating at head. if (!current.valid) { current = new InputEventPtr((InputEvent*)m_EventBufferHead); return true; } // Otherwise feel our way forward. var nextEvent = (byte*)current.Next().data; var endOfBuffer = m_EventBuffer + m_EventBufferSize; // If we've run into our tail, there's no more events. if (nextEvent == m_EventBufferTail) return false; // If we've reached blank space at the end of the buffer, wrap // around to the beginning. In this scenario there must be an event // at the beginning of the buffer; tail won't position itself at // m_EventBuffer. if (endOfBuffer - nextEvent < InputEvent.kBaseEventSize || ((InputEvent*)nextEvent)->sizeInBytes == 0) { nextEvent = m_EventBuffer; if (nextEvent == current.ToPointer()) return false; // There's only a single event in the buffer. } // We're good. There's still space between us and our tail. current = new InputEventPtr((InputEvent*)nextEvent); return true; } public IEnumerator GetEnumerator() { return new Enumerator(this); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// /// Stop recording, if necessary, and clear the trace such that it released unmanaged /// memory which might be allocated. /// /// /// For any trace that has recorded events, calling this method is crucial in order to not /// leak memory on the unmanaged (C++) memory heap. /// public void Dispose() { Disable(); Release(); } // We want to make sure that it's not possible to iterate with an enumerable over // a trace that is being changed so we bump this counter every time we modify the // buffer and check in the enumerator that the counts match. [NonSerialized] private int m_ChangeCounter; [NonSerialized] private bool m_Enabled; [NonSerialized] private Func m_OnFilterEvent; [SerializeField] private int m_DeviceId = InputDevice.InvalidDeviceId; [NonSerialized] private CallbackArray> m_EventListeners; // Buffer for storing event trace. Allocated in native so that we can survive a // domain reload without losing event traces. // NOTE: Ideally this would simply use InputEventBuffer but we can't serialize that one because // of the NativeArray it has inside. Also, due to the wrap-around nature, storage of // events in the buffer may not be linear. [SerializeField] private long m_EventBufferSize; [SerializeField] private long m_MaxEventBufferSize; [SerializeField] private long m_GrowIncrementSize; [SerializeField] private long m_EventCount; [SerializeField] private long m_EventSizeInBytes; // These are ulongs for the sake of Unity serialization which can't handle pointers or IntPtrs. [SerializeField] private ulong m_EventBufferStorage; [SerializeField] private ulong m_EventBufferHeadStorage; [SerializeField] private ulong m_EventBufferTailStorage; [SerializeField] private bool m_HasWrapped; [SerializeField] private bool m_RecordFrameMarkers; [SerializeField] private DeviceInfo[] m_DeviceInfos; private byte* m_EventBuffer { get => (byte*)m_EventBufferStorage; set => m_EventBufferStorage = (ulong)value; } private byte* m_EventBufferHead { get => (byte*)m_EventBufferHeadStorage; set => m_EventBufferHeadStorage = (ulong)value; } private byte* m_EventBufferTail { get => (byte*)m_EventBufferTailStorage; set => m_EventBufferTailStorage = (ulong)value; } private void Allocate() { m_EventBuffer = (byte*)UnsafeUtility.Malloc(m_EventBufferSize, InputEvent.kAlignment, Allocator.Persistent); } private void Release() { Clear(); if (m_EventBuffer != default) { UnsafeUtility.Free(m_EventBuffer, Allocator.Persistent); m_EventBuffer = default; } } private void OnBeforeUpdate() { ////TODO: make this work correctly with the different update types if (m_RecordFrameMarkers) { // Record frame marker event. // NOTE: ATM these events don't get valid event IDs. Might be this is even useful but is more a side-effect // of there not being a method to obtain an ID except by actually queuing an event. var frameMarkerEvent = new InputEvent { type = FrameMarkerEvent, internalTime = InputRuntime.s_Instance.currentTime, sizeInBytes = (uint)UnsafeUtility.SizeOf() }; OnInputEvent(new InputEventPtr((InputEvent*)UnsafeUtility.AddressOf(ref frameMarkerEvent)), null); } } private void OnInputEvent(InputEventPtr inputEvent, InputDevice device) { // Ignore events that are already marked as handled. if (inputEvent.handled) return; // Ignore if the event isn't for our device (except if it's a frame marker). if (m_DeviceId != InputDevice.InvalidDeviceId && inputEvent.deviceId != m_DeviceId && inputEvent.type != FrameMarkerEvent) return; // Give callback a chance to filter event. if (m_OnFilterEvent != null && !m_OnFilterEvent(inputEvent, device)) return; // This shouldn't happen but ignore the event if we're not tracing. if (m_EventBuffer == default) return; var bytesNeeded = inputEvent.sizeInBytes.AlignToMultipleOf(InputEvent.kAlignment); // Make sure we can fit the event at all. if (bytesNeeded > m_MaxEventBufferSize) return; Profiler.BeginSample("InputEventTrace"); if (m_EventBufferTail == default) { // First event in buffer. m_EventBufferHead = m_EventBuffer; m_EventBufferTail = m_EventBuffer; } var newTail = m_EventBufferTail + bytesNeeded; var newTailOvertakesHead = newTail > m_EventBufferHead && m_EventBufferHead != m_EventBuffer; // If tail goes out of bounds, enlarge the buffer or wrap around to the beginning. var newTailGoesPastEndOfBuffer = newTail > m_EventBuffer + m_EventBufferSize; if (newTailGoesPastEndOfBuffer) { // If we haven't reached the max size yet, grow the buffer. if (m_EventBufferSize < m_MaxEventBufferSize && !m_HasWrapped) { var increment = Math.Max(m_GrowIncrementSize, bytesNeeded.AlignToMultipleOf(InputEvent.kAlignment)); var newBufferSize = m_EventBufferSize + increment; if (newBufferSize > m_MaxEventBufferSize) newBufferSize = m_MaxEventBufferSize; if (newBufferSize < bytesNeeded) return; Resize(newBufferSize); newTail = m_EventBufferTail + bytesNeeded; } // See if we fit. var spaceLeft = m_EventBufferSize - (m_EventBufferTail - m_EventBuffer); if (spaceLeft < bytesNeeded) { // No, so wrap around. m_HasWrapped = true; // Make sure head isn't trying to advance into gap we may be leaving at the end of the // buffer by wiping the space if it could fit an event. if (spaceLeft >= InputEvent.kBaseEventSize) UnsafeUtility.MemClear(m_EventBufferTail, InputEvent.kBaseEventSize); m_EventBufferTail = m_EventBuffer; newTail = m_EventBuffer + bytesNeeded; // If the tail overtook both the head and the end of the buffer, // we need to make sure the head is wrapped around as well. if (newTailOvertakesHead) m_EventBufferHead = m_EventBuffer; // Recheck whether we're overtaking head. newTailOvertakesHead = newTail > m_EventBufferHead; } } // If the new tail runs into head, bump head as many times as we need to // make room for the event. Head may itself wrap around here. if (newTailOvertakesHead) { var newHead = m_EventBufferHead; var endOfBufferMinusOneEvent = m_EventBuffer + m_EventBufferSize - InputEvent.kBaseEventSize; while (newHead < newTail) { var numBytes = ((InputEvent*)newHead)->sizeInBytes; newHead += numBytes; --m_EventCount; m_EventSizeInBytes -= numBytes; if (newHead > endOfBufferMinusOneEvent || ((InputEvent*)newHead)->sizeInBytes == 0) { newHead = m_EventBuffer; break; } } m_EventBufferHead = newHead; } var buffer = m_EventBufferTail; m_EventBufferTail = newTail; // Copy data to buffer. UnsafeUtility.MemCpy(buffer, inputEvent.data, inputEvent.sizeInBytes); ++m_ChangeCounter; ++m_EventCount; m_EventSizeInBytes += bytesNeeded; // Make sure we have a record for the device. if (device != null) { var haveRecord = false; if (m_DeviceInfos != null) for (var i = 0; i < m_DeviceInfos.Length; ++i) if (m_DeviceInfos[i].deviceId == device.deviceId) { haveRecord = true; break; } if (!haveRecord) ArrayHelpers.Append(ref m_DeviceInfos, new DeviceInfo { m_DeviceId = device.deviceId, m_Layout = device.layout, m_StateFormat = device.stateBlock.format, m_StateSizeInBytes = (int)device.stateBlock.alignedSizeInBytes, // If it's a generated layout, store the full layout JSON in the device info. We do this so that // when saving traces for this kind of input, we can recreate the device. m_FullLayoutJson = InputControlLayout.s_Layouts.IsGeneratedLayout(device.m_Layout) ? InputSystem.LoadLayout(device.layout).ToJson() : null }); } // Notify listeners. if (m_EventListeners.length > 0) DelegateHelpers.InvokeCallbacksSafe(ref m_EventListeners, new InputEventPtr((InputEvent*)buffer), "InputEventTrace.onEvent"); Profiler.EndSample(); } private class Enumerator : IEnumerator { private InputEventTrace m_Trace; private int m_ChangeCounter; internal InputEventPtr m_Current; public Enumerator(InputEventTrace trace) { m_Trace = trace; m_ChangeCounter = trace.m_ChangeCounter; } public void Dispose() { m_Trace = null; m_Current = new InputEventPtr(); } public bool MoveNext() { if (m_Trace == null) throw new ObjectDisposedException(ToString()); if (m_Trace.m_ChangeCounter != m_ChangeCounter) throw new InvalidOperationException("Trace has been modified while enumerating!"); return m_Trace.GetNextEvent(ref m_Current); } public void Reset() { m_Current = default; m_ChangeCounter = m_Trace.m_ChangeCounter; } public InputEventPtr Current => m_Current; object IEnumerator.Current => Current; } private static FourCC kFileFormat => new FourCC('I', 'E', 'V', 'T'); private static int kFileVersion = 1; [Flags] private enum FileFlags { FixedUpdate = 1 << 0, // Events were recorded with system being in fixed-update mode. } /// /// Controls replaying of events recorded in an . /// /// /// Playback can be controlled either on a per-event or a per-frame basis. Note that playing back events /// frame by frame requires frame markers to be present in the trace (see ). /// /// By default, events will be queued as is except for their timestamps which will be set to the current /// time that each event is queued at. /// /// What this means is that events replay with the same device ID (see ) /// they were captured on. If the trace is replayed in the same session that it was recorded in, this means /// that the events will replay on the same device (if it still exists). /// /// To map recorded events to a different device, you can either call to /// map an arbitrary device ID to a new one or call to create /// new (temporary) devices for the duration of playback. /// /// /// /// var trace = new InputEventTrace(myDevice); /// trace.Enable(); /// /// // ... run one or more frames ... /// /// trace.Replay().OneFrame(); /// /// /// /// public class ReplayController : IDisposable { /// /// The event trace associated with the replay controller. /// /// Trace from which events are replayed. public InputEventTrace trace => m_EventTrace; /// /// Whether replay has finished. /// /// True if replay has finished or is not in progress. /// /// public bool finished { get; private set; } /// /// Whether replay is paused. /// /// True if replay is currently paused. public bool paused { get; set; } /// /// Current position in the event stream. /// /// Index of current event in trace. public int position { get; private set; } /// /// List of devices created by the replay controller. /// /// Devices created by the replay controller. /// /// By default, a replay controller will queue events as is, i.e. with of /// each event left as is. This means that the events will target existing devices (if any) that have the /// respective ID. /// /// Using , a replay controller can be instructed to create /// new, temporary devices instead for each unique encountered in the stream. /// All devices created by the controller this way will be put on this list. /// /// public IEnumerable createdDevices => m_CreatedDevices; private InputEventTrace m_EventTrace; private Enumerator m_Enumerator; private InlinedArray> m_DeviceIDMappings; private bool m_CreateNewDevices; private InlinedArray m_CreatedDevices; private Action m_OnFinished; private Action m_OnEvent; private double m_StartTimeAsPerFirstEvent; private double m_StartTimeAsPerRuntime; private int m_AllEventsByTimeIndex = 0; private List m_AllEventsByTime; internal ReplayController(InputEventTrace trace) { if (trace == null) throw new ArgumentNullException(nameof(trace)); m_EventTrace = trace; } /// /// Removes devices created by the controller when using . /// public void Dispose() { InputSystem.onBeforeUpdate -= OnBeginFrame; finished = true; foreach (var device in m_CreatedDevices) InputSystem.RemoveDevice(device); m_CreatedDevices = default; } /// /// Replay events recorded from on device . /// /// Device events have been recorded from. /// Device events should be played back on. /// The same ReplayController instance. /// is null -or- /// is null. /// /// This method causes all events with a device ID (see and ) /// corresponding to the one of to be queued with the device ID of . /// public ReplayController WithDeviceMappedFromTo(InputDevice recordedDevice, InputDevice playbackDevice) { if (recordedDevice == null) throw new ArgumentNullException(nameof(recordedDevice)); if (playbackDevice == null) throw new ArgumentNullException(nameof(playbackDevice)); WithDeviceMappedFromTo(recordedDevice.deviceId, playbackDevice.deviceId); return this; } /// /// Replace values of events that are equal to /// with device ID . /// /// to map from. /// to map to. /// The same ReplayController instance. public ReplayController WithDeviceMappedFromTo(int recordedDeviceId, int playbackDeviceId) { // If there's an existing mapping entry for the device, update it. for (var i = 0; i < m_DeviceIDMappings.length; ++i) { if (m_DeviceIDMappings[i].Key != recordedDeviceId) continue; if (recordedDeviceId == playbackDeviceId) // Device mapped back to itself. m_DeviceIDMappings.RemoveAtWithCapacity(i); else m_DeviceIDMappings[i] = new KeyValuePair(recordedDeviceId, playbackDeviceId); return this; } // Ignore if mapped to itself. if (recordedDeviceId == playbackDeviceId) return this; // Record mapping. m_DeviceIDMappings.AppendWithCapacity(new KeyValuePair(recordedDeviceId, playbackDeviceId)); return this; } /// /// For all events, create new devices to replay the events on instead of replaying the events on existing devices. /// /// The same ReplayController instance. /// /// Note that devices created by the ReplayController will stick around for as long as the replay /// controller is not disposed of. This means that multiple successive replays using the same ReplayController /// will replay the events on the same devices that were created on the first replay. It also means that in order /// to do away with the created devices, it is necessary to call . /// /// /// public ReplayController WithAllDevicesMappedToNewInstances() { m_CreateNewDevices = true; return this; } /// /// Invoke the given callback when playback finishes. /// /// A callback to invoke when playback finishes. /// The same ReplayController instance. public ReplayController OnFinished(Action action) { m_OnFinished = action; return this; } /// /// Invoke the given callback when an event is about to be queued. /// /// A callback to invoke when an event is getting queued. /// The same ReplayController instance. public ReplayController OnEvent(Action action) { m_OnEvent = action; return this; } /// /// Takes the next event from the trace and queues it. /// /// The same ReplayController instance. /// There are no more events in the -or- the only /// events left are frame marker events (see ). /// /// This method takes the next event at the current read position and queues it using . /// The read position is advanced past the taken event. /// /// Frame marker events (see ) are skipped. /// public ReplayController PlayOneEvent() { // Skip events until we hit something that isn't a frame marker. if (!MoveNext(true, out var eventPtr)) throw new InvalidOperationException("No more events"); QueueEvent(eventPtr); return this; } ////TODO: OneFrame ////TODO: RewindOneEvent ////TODO: RewindOneFrame ////TODO: Stop /// /// Rewind playback all the way to the beginning of the event trace. /// /// The same ReplayController instance. public ReplayController Rewind() { m_Enumerator = default; m_AllEventsByTime = null; m_AllEventsByTimeIndex = -1; position = 0; return this; } /// /// Replay all frames one by one from the current playback position. /// /// The same ReplayController instance. /// /// Events will be fed to the input system from within . Each update /// will receive events for one frame. /// /// Note that for this method to correctly space out events and distribute them to frames, frame markers /// must be present in the trace (see ). If not present, all events will /// be fed into first frame. /// /// /// /// /// public ReplayController PlayAllFramesOneByOne() { finished = false; InputSystem.onBeforeUpdate += OnBeginFrame; return this; } /// /// Go through all remaining event in the trace starting at the current read position and queue them using /// . /// /// The same ReplayController instance. /// /// Unlike methods such as , this method immediately queues events and immediately /// completes playback upon return from the method. /// /// /// public ReplayController PlayAllEvents() { finished = false; try { while (MoveNext(true, out var eventPtr)) QueueEvent(eventPtr); } finally { Finished(); } return this; } /// /// Replay events in a way that tries to preserve the original timing sequence. /// /// The same ReplayController instance. /// /// This method will take the current time as the starting time to which make all events /// relative to. Based on this time, it will try to correlate the original event timing /// with the timing of input updates as they happen. When successful, this will compensate /// for differences in frame timings compared to when input was recorded and instead queue /// input in frames that are closer to the original timing. /// /// Note that this method will perform one initial scan of the trace to determine a linear /// ordering of the events by time (the input system does not require any such ordering on the /// events in its queue and thus events in a trace, especially if there are multiple devices /// involved, may be out of order). /// /// /// public ReplayController PlayAllEventsAccordingToTimestamps() { // Sort remaining events by time. var eventsByTime = new List(); while (MoveNext(true, out var eventPtr)) eventsByTime.Add(eventPtr); eventsByTime.Sort((a, b) => a.time.CompareTo(b.time)); m_Enumerator.Dispose(); m_Enumerator = null; m_AllEventsByTime = eventsByTime; position = 0; // Start playback. finished = false; m_StartTimeAsPerFirstEvent = -1; m_AllEventsByTimeIndex = -1; InputSystem.onBeforeUpdate += OnBeginFrame; return this; } private void OnBeginFrame() { if (paused) return; if (!MoveNext(false, out var currentEventPtr)) { if (m_AllEventsByTime == null || m_AllEventsByTimeIndex >= m_AllEventsByTime.Count) Finished(); return; } // Check for empty frame (note: when playing back events by time, we won't see frame marker events // returned from MoveNext). if (currentEventPtr.type == FrameMarkerEvent) { if (!MoveNext(false, out var nextEvent)) { // Last frame. Finished(); return; } // Check for empty frame. if (nextEvent.type == FrameMarkerEvent) { --position; m_Enumerator.m_Current = currentEventPtr; return; } currentEventPtr = nextEvent; } // Inject our events into the frame. while (true) { QueueEvent(currentEventPtr); // Stop if we reach the end of the stream. if (!MoveNext(false, out var nextEvent)) { if (m_AllEventsByTime == null || m_AllEventsByTimeIndex >= m_AllEventsByTime.Count) Finished(); break; } // Stop if we've reached the next frame (won't happen if we're playing events by time). if (nextEvent.type == FrameMarkerEvent) { // Back up one event. m_Enumerator.m_Current = currentEventPtr; --position; break; } currentEventPtr = nextEvent; } } private void Finished() { finished = true; InputSystem.onBeforeUpdate -= OnBeginFrame; m_OnFinished?.Invoke(); } private void QueueEvent(InputEventPtr eventPtr) { // Shift time on event. var originalTimestamp = eventPtr.internalTime; if (m_AllEventsByTime != null) eventPtr.internalTime = m_StartTimeAsPerRuntime + (eventPtr.internalTime - m_StartTimeAsPerFirstEvent); else eventPtr.internalTime = InputRuntime.s_Instance.currentTime; // Remember original event ID. QueueEvent will automatically update the event ID // and actually do so in place. var originalEventId = eventPtr.id; // Map device ID. var originalDeviceId = eventPtr.deviceId; eventPtr.deviceId = ApplyDeviceMapping(originalDeviceId); // Notify. m_OnEvent?.Invoke(eventPtr); // Queue event. try { InputSystem.QueueEvent(eventPtr); } finally { // Restore modification we made to the event buffer. eventPtr.internalTime = originalTimestamp; eventPtr.id = originalEventId; eventPtr.deviceId = originalDeviceId; } } private bool MoveNext(bool skipFrameEvents, out InputEventPtr eventPtr) { eventPtr = default; if (m_AllEventsByTime != null) { if (m_AllEventsByTimeIndex + 1 >= m_AllEventsByTime.Count) { position = m_AllEventsByTime.Count; m_AllEventsByTimeIndex = m_AllEventsByTime.Count; return false; } if (m_AllEventsByTimeIndex < 0) { m_StartTimeAsPerFirstEvent = m_AllEventsByTime[0].internalTime; m_StartTimeAsPerRuntime = InputRuntime.s_Instance.currentTime; } else if (m_AllEventsByTimeIndex < m_AllEventsByTime.Count - 1 && m_AllEventsByTime[m_AllEventsByTimeIndex + 1].internalTime > m_StartTimeAsPerFirstEvent + (InputRuntime.s_Instance.currentTime - m_StartTimeAsPerRuntime)) { // We're queuing by original time and the next event isn't up yet, // so early out. return false; } ++m_AllEventsByTimeIndex; ++position; eventPtr = m_AllEventsByTime[m_AllEventsByTimeIndex]; } else { if (m_Enumerator == null) m_Enumerator = new Enumerator(m_EventTrace); do { if (!m_Enumerator.MoveNext()) return false; ++position; eventPtr = m_Enumerator.Current; } while (skipFrameEvents && eventPtr.type == FrameMarkerEvent); } return true; } private int ApplyDeviceMapping(int originalDeviceId) { // Look up in mappings. for (var i = 0; i < m_DeviceIDMappings.length; ++i) { var entry = m_DeviceIDMappings[i]; if (entry.Key == originalDeviceId) return entry.Value; } // Create device, if needed. if (m_CreateNewDevices) { try { // Find device info. var deviceIndex = m_EventTrace.deviceInfos.IndexOf(x => x.deviceId == originalDeviceId); if (deviceIndex != -1) { var deviceInfo = m_EventTrace.deviceInfos[deviceIndex]; // If we don't have the layout, try to add it from the persisted layout info. var layoutName = new InternedString(deviceInfo.layout); if (!InputControlLayout.s_Layouts.HasLayout(layoutName)) { if (string.IsNullOrEmpty(deviceInfo.m_FullLayoutJson)) return originalDeviceId; InputSystem.RegisterLayout(deviceInfo.m_FullLayoutJson); } // Create device. var device = InputSystem.AddDevice(layoutName); WithDeviceMappedFromTo(originalDeviceId, device.deviceId); m_CreatedDevices.AppendWithCapacity(device); return device.deviceId; } } catch { // Swallow and just return originalDeviceId. } } return originalDeviceId; } } /// /// Information about a device whose input has been captured in an /// /// [Serializable] public struct DeviceInfo { /// /// Id of the device as stored in the events for the device. /// /// public int deviceId { get => m_DeviceId; set => m_DeviceId = value; } /// /// Name of the layout used by the device. /// /// public string layout { get => m_Layout; set => m_Layout = value; } /// /// Tag for the format in which state for the device is stored. /// /// /// public FourCC stateFormat { get => m_StateFormat; set => m_StateFormat = value; } /// /// Size of a full state snapshot of the device. /// public int stateSizeInBytes { get => m_StateSizeInBytes; set => m_StateSizeInBytes = value; } [SerializeField] internal int m_DeviceId; [SerializeField] internal string m_Layout; [SerializeField] internal FourCC m_StateFormat; [SerializeField] internal int m_StateSizeInBytes; [SerializeField] internal string m_FullLayoutJson; } } }