Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Editor /Internal /InputStateWindow.cs
| using System; | |
| using System.Collections.Generic; | |
| using Unity.Collections.LowLevel.Unsafe; | |
| using UnityEditor; | |
| using UnityEditor.IMGUI.Controls; | |
| using UnityEngine.InputSystem.LowLevel; | |
| ////TODO: add ability to single-step through events | |
| ////TODO: annotate raw memory view with control offset and ranges (probably easiest to put the control tree and raw memory view side by side) | |
| ////TODO: find way to automatically dock the state windows next to their InputDeviceDebuggerWindows | |
| //// (probably needs an extension to the editor UI APIs as the only programmatic docking controls | |
| //// seem to be through GetWindow) | |
| ////TODO: allow setting a C# struct type that we can use to display the layout of the data | |
| ////TODO: for delta state events, highlight the controls included in the event (or show only those) | |
| ////FIXME: need to prevent extra controls appended at end from reading beyond the state buffer | |
| namespace UnityEngine.InputSystem.Editor | |
| { | |
| // Additional window that we can pop open to inspect raw state (either on events or on controls/devices). | |
| internal class InputStateWindow : EditorWindow | |
| { | |
| private const int kBytesPerHexGroup = 1; | |
| private const int kHexGroupsPerLine = 8; | |
| private const int kHexDumpLineHeight = 25; | |
| private const int kOffsetLabelWidth = 30; | |
| private const int kHexGroupWidth = 25; | |
| private const int kBitGroupWidth = 75; | |
| void Update() | |
| { | |
| if (m_PollControlState && m_Control != null) | |
| { | |
| PollBuffersFromControl(m_Control); | |
| Repaint(); | |
| } | |
| } | |
| public void InitializeWithEvent(InputEventPtr eventPtr, InputControl control) | |
| { | |
| m_Control = control; | |
| m_PollControlState = false; | |
| m_StateBuffers = new byte[1][]; | |
| m_StateBuffers[0] = GetEventStateBuffer(eventPtr, control); | |
| m_SelectedStateBuffer = 0; | |
| titleContent = new GUIContent(control.displayName); | |
| } | |
| public void InitializeWithEvents(InputEventPtr[] eventPtrs, InputControl control) | |
| { | |
| var numEvents = eventPtrs.Length; | |
| m_Control = control; | |
| m_PollControlState = false; | |
| m_StateBuffers = new byte[numEvents][]; | |
| for (var i = 0; i < numEvents; ++i) | |
| m_StateBuffers[i] = GetEventStateBuffer(eventPtrs[i], control); | |
| m_CompareStateBuffers = true; | |
| m_ShowDifferentOnly = true; | |
| titleContent = new GUIContent(control.displayName); | |
| } | |
| private unsafe byte[] GetEventStateBuffer(InputEventPtr eventPtr, InputControl control) | |
| { | |
| // Must be an event carrying state. | |
| if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>()) | |
| throw new ArgumentException("Event must be state or delta event", nameof(eventPtr)); | |
| // Get state data. | |
| void* dataPtr; | |
| uint dataSize; | |
| uint stateSize; | |
| uint stateOffset = 0; | |
| if (eventPtr.IsA<DeltaStateEvent>()) | |
| { | |
| var deltaEventPtr = DeltaStateEvent.From(eventPtr); | |
| stateSize = control.stateBlock.alignedSizeInBytes; | |
| stateOffset = deltaEventPtr->stateOffset; | |
| dataPtr = deltaEventPtr->deltaState; | |
| dataSize = deltaEventPtr->deltaStateSizeInBytes; | |
| } | |
| else | |
| { | |
| var stateEventPtr = StateEvent.From(eventPtr); | |
| dataSize = stateSize = stateEventPtr->stateSizeInBytes; | |
| dataPtr = stateEventPtr->state; | |
| } | |
| // Copy event data. | |
| var buffer = new byte[stateSize]; | |
| fixed(byte* bufferPtr = buffer) | |
| { | |
| UnsafeUtility.MemCpy(bufferPtr + stateOffset, dataPtr, dataSize); | |
| } | |
| return buffer; | |
| } | |
| public unsafe void InitializeWithControl(InputControl control) | |
| { | |
| m_Control = control; | |
| m_PollControlState = true; | |
| m_SelectedStateBuffer = (int)BufferSelector.Default; | |
| PollBuffersFromControl(control, selectBuffer: true); | |
| titleContent = new GUIContent(control.displayName); | |
| } | |
| private unsafe void PollBuffersFromControl(InputControl control, bool selectBuffer = false) | |
| { | |
| var bufferChoices = new List<GUIContent>(); | |
| var bufferChoiceValues = new List<int>(); | |
| // Copy front and back buffer state for each update that has valid buffers. | |
| var device = control.device; | |
| var stateSize = control.m_StateBlock.alignedSizeInBytes; | |
| var stateOffset = control.m_StateBlock.byteOffset; | |
| m_StateBuffers = new byte[(int)BufferSelector.COUNT][]; | |
| for (var i = 0; i < (int)BufferSelector.COUNT; ++i) | |
| { | |
| var selector = (BufferSelector)i; | |
| var deviceState = TryGetDeviceState(device, selector); | |
| if (deviceState == null) | |
| continue; | |
| var buffer = new byte[stateSize]; | |
| fixed(byte* stateDataPtr = buffer) | |
| { | |
| UnsafeUtility.MemCpy(stateDataPtr, (byte*)deviceState + (int)stateOffset, stateSize); | |
| } | |
| m_StateBuffers[i] = buffer; | |
| if (selectBuffer && m_StateBuffers[m_SelectedStateBuffer] == null) | |
| m_SelectedStateBuffer = (int)selector; | |
| bufferChoices.Add(Contents.bufferChoices[i]); | |
| bufferChoiceValues.Add(i); | |
| } | |
| m_BufferChoices = bufferChoices.ToArray(); | |
| m_BufferChoiceValues = bufferChoiceValues.ToArray(); | |
| } | |
| private static unsafe void* TryGetDeviceState(InputDevice device, BufferSelector selector) | |
| { | |
| var manager = InputSystem.s_Manager; | |
| var deviceIndex = device.m_DeviceIndex; | |
| switch (selector) | |
| { | |
| case BufferSelector.PlayerUpdateFrontBuffer: | |
| if (manager.m_StateBuffers.m_PlayerStateBuffers.valid) | |
| return manager.m_StateBuffers.m_PlayerStateBuffers.GetFrontBuffer(deviceIndex); | |
| break; | |
| case BufferSelector.PlayerUpdateBackBuffer: | |
| if (manager.m_StateBuffers.m_PlayerStateBuffers.valid) | |
| return manager.m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex); | |
| break; | |
| case BufferSelector.EditorUpdateFrontBuffer: | |
| if (manager.m_StateBuffers.m_EditorStateBuffers.valid) | |
| return manager.m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex); | |
| break; | |
| case BufferSelector.EditorUpdateBackBuffer: | |
| if (manager.m_StateBuffers.m_EditorStateBuffers.valid) | |
| return manager.m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex); | |
| break; | |
| case BufferSelector.NoiseMaskBuffer: | |
| return manager.m_StateBuffers.noiseMaskBuffer; | |
| case BufferSelector.ResetMaskBuffer: | |
| return manager.m_StateBuffers.resetMaskBuffer; | |
| } | |
| return null; | |
| } | |
| public void OnGUI() | |
| { | |
| if (m_Control == null) | |
| m_ShowRawBytes = true; | |
| // If our state is no longer valid, just close the window. | |
| if (m_StateBuffers == null) | |
| { | |
| Close(); | |
| return; | |
| } | |
| GUILayout.BeginHorizontal(EditorStyles.toolbar); | |
| m_PollControlState = GUILayout.Toggle(m_PollControlState, Contents.live, EditorStyles.toolbarButton); | |
| m_ShowRawBytes = GUILayout.Toggle(m_ShowRawBytes, Contents.showRawMemory, EditorStyles.toolbarButton, | |
| GUILayout.Width(150)); | |
| m_ShowAsBits = GUILayout.Toggle(m_ShowAsBits, Contents.showBits, EditorStyles.toolbarButton); | |
| if (m_CompareStateBuffers) | |
| { | |
| var showDifferentOnly = GUILayout.Toggle(m_ShowDifferentOnly, Contents.showDifferentOnly, | |
| EditorStyles.toolbarButton, GUILayout.Width(150)); | |
| if (showDifferentOnly != m_ShowDifferentOnly && m_ControlTree != null) | |
| { | |
| m_ControlTree.showDifferentOnly = showDifferentOnly; | |
| m_ControlTree.Reload(); | |
| } | |
| m_ShowDifferentOnly = showDifferentOnly; | |
| } | |
| // If we have multiple state buffers to choose from and we're not comparing them to each other, | |
| // add dropdown that allows selecting which buffer to display. | |
| if (m_StateBuffers.Length > 1 && !m_CompareStateBuffers) | |
| { | |
| var selectedBuffer = EditorGUILayout.IntPopup(m_SelectedStateBuffer, m_BufferChoices, | |
| m_BufferChoiceValues, EditorStyles.toolbarPopup); | |
| if (selectedBuffer != m_SelectedStateBuffer) | |
| { | |
| m_SelectedStateBuffer = selectedBuffer; | |
| m_ControlTree = null; | |
| } | |
| } | |
| GUILayout.FlexibleSpace(); | |
| GUILayout.EndHorizontal(); | |
| if (m_ShowRawBytes) | |
| { | |
| DrawHexDump(); | |
| } | |
| else | |
| { | |
| if (m_ControlTree == null) | |
| { | |
| if (m_CompareStateBuffers) | |
| { | |
| m_ControlTree = InputControlTreeView.Create(m_Control, m_StateBuffers.Length, ref m_ControlTreeState, ref m_ControlTreeHeaderState); | |
| m_ControlTree.multipleStateBuffers = m_StateBuffers; | |
| m_ControlTree.showDifferentOnly = m_ShowDifferentOnly; | |
| } | |
| else | |
| { | |
| m_ControlTree = InputControlTreeView.Create(m_Control, 1, ref m_ControlTreeState, ref m_ControlTreeHeaderState); | |
| m_ControlTree.stateBuffer = m_StateBuffers[m_SelectedStateBuffer]; | |
| } | |
| m_ControlTree.Reload(); | |
| m_ControlTree.ExpandAll(); | |
| } | |
| var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true)); | |
| m_ControlTree.OnGUI(rect); | |
| } | |
| } | |
| private byte[] TryGetBackBufferForCurrentlySelected() | |
| { | |
| if (m_StateBuffers.Length != (int)BufferSelector.COUNT) | |
| return null; | |
| switch ((BufferSelector)m_SelectedStateBuffer) | |
| { | |
| case BufferSelector.PlayerUpdateFrontBuffer: | |
| return m_StateBuffers[(int)BufferSelector.PlayerUpdateBackBuffer]; | |
| case BufferSelector.EditorUpdateFrontBuffer: | |
| return m_StateBuffers[(int)BufferSelector.EditorUpdateBackBuffer]; | |
| default: | |
| return null; | |
| } | |
| } | |
| private string FormatByte(byte value) | |
| { | |
| if (m_ShowAsBits) | |
| return Convert.ToString(value, 2).PadLeft(8, '0'); | |
| else | |
| return value.ToString("X2"); | |
| } | |
| ////TODO: support dumping multiple state side-by-side when comparing | |
| private void DrawHexDump() | |
| { | |
| m_HexDumpScrollPosition = EditorGUILayout.BeginScrollView(m_HexDumpScrollPosition); | |
| var stateBuffer = m_StateBuffers[m_SelectedStateBuffer]; | |
| var prevStateBuffer = TryGetBackBufferForCurrentlySelected(); | |
| if (prevStateBuffer != null && prevStateBuffer.Length != stateBuffer.Length) // we assume they're same length, otherwise ignore prev buffer | |
| prevStateBuffer = null; | |
| var numBytes = stateBuffer.Length; | |
| var numHexGroups = numBytes / kBytesPerHexGroup + (numBytes % kBytesPerHexGroup > 0 ? 1 : 0); | |
| var numLines = numHexGroups / kHexGroupsPerLine + (numHexGroups % kHexGroupsPerLine > 0 ? 1 : 0); | |
| var currentOffset = 0; | |
| var currentLineRect = EditorGUILayout.GetControlRect(GUILayout.ExpandWidth(true)); | |
| currentLineRect.height = kHexDumpLineHeight; | |
| var currentHexGroup = 0; | |
| var currentByte = 0; | |
| ////REVIEW: what would be totally awesome is if this not just displayed a hex dump but also the correlation to current | |
| //// control offset assignments | |
| for (var line = 0; line < numLines; ++line) | |
| { | |
| // Draw offset. | |
| var offsetLabelRect = currentLineRect; | |
| offsetLabelRect.width = kOffsetLabelWidth; | |
| GUI.Label(offsetLabelRect, currentOffset.ToString(), Styles.offsetLabel); | |
| currentOffset += kBytesPerHexGroup * kHexGroupsPerLine; | |
| // Draw hex groups. | |
| var hexGroupRect = offsetLabelRect; | |
| hexGroupRect.x += kOffsetLabelWidth + 10; | |
| hexGroupRect.width = m_ShowAsBits ? kBitGroupWidth : kHexGroupWidth; | |
| for (var group = 0; | |
| group < kHexGroupsPerLine && currentHexGroup < numHexGroups; | |
| ++group, ++currentHexGroup) | |
| { | |
| // Convert bytes to hex. | |
| var hex = string.Empty; | |
| for (var i = 0; i < kBytesPerHexGroup; ++i, ++currentByte) | |
| { | |
| if (currentByte >= numBytes) | |
| { | |
| hex += " "; | |
| continue; | |
| } | |
| var current = FormatByte(stateBuffer[currentByte]); | |
| if (prevStateBuffer == null) | |
| { | |
| hex += current; | |
| continue; | |
| } | |
| var prev = FormatByte(prevStateBuffer[currentByte]); | |
| if (prev.Length != current.Length) | |
| { | |
| hex += current; | |
| continue; | |
| } | |
| for (var j = 0; j < current.Length; ++j) | |
| { | |
| if (current[j] != prev[j]) | |
| hex += $"<color=#C84B31FF>{current[j]}</color>"; | |
| else | |
| hex += current[j]; | |
| } | |
| } | |
| ////TODO: draw alternating backgrounds for the hex groups | |
| GUI.Label(hexGroupRect, hex, style: Styles.hexLabel); | |
| hexGroupRect.x += m_ShowAsBits ? kBitGroupWidth : kHexGroupWidth; | |
| } | |
| currentLineRect.y += kHexDumpLineHeight; | |
| } | |
| EditorGUILayout.EndScrollView(); | |
| } | |
| // We copy the state we're inspecting to a buffer we own so that we're safe | |
| // against any mutations. | |
| // When inspecting controls (as opposed to events), we copy all their various | |
| // state buffers and allow switching between them. | |
| [] private byte[][] m_StateBuffers; | |
| [] private int m_SelectedStateBuffer; | |
| [] private bool m_CompareStateBuffers; | |
| [] private bool m_ShowDifferentOnly; | |
| [] private bool m_ShowRawBytes; | |
| [] private bool m_ShowAsBits; | |
| [] private bool m_PollControlState; | |
| [] private TreeViewState m_ControlTreeState; | |
| [] private MultiColumnHeaderState m_ControlTreeHeaderState; | |
| [] private Vector2 m_HexDumpScrollPosition; | |
| [] private InputControlTreeView m_ControlTree; | |
| [] private GUIContent[] m_BufferChoices; | |
| [] private int[] m_BufferChoiceValues; | |
| ////FIXME: we lose this on domain reload; how should we recover? | |
| [] private InputControl m_Control; | |
| private enum BufferSelector | |
| { | |
| PlayerUpdateFrontBuffer, | |
| PlayerUpdateBackBuffer, | |
| EditorUpdateFrontBuffer, | |
| EditorUpdateBackBuffer, | |
| NoiseMaskBuffer, | |
| ResetMaskBuffer, | |
| COUNT, | |
| Default = PlayerUpdateFrontBuffer | |
| } | |
| private static class Styles | |
| { | |
| public static GUIStyle offsetLabel = new GUIStyle | |
| { | |
| alignment = TextAnchor.UpperRight, | |
| fontStyle = FontStyle.BoldAndItalic, | |
| font = EditorStyles.boldFont, | |
| fontSize = EditorStyles.boldFont.fontSize - 2, | |
| normal = new GUIStyleState { textColor = Color.black } | |
| }; | |
| public static GUIStyle hexLabel = new GUIStyle | |
| { | |
| fontStyle = FontStyle.Normal, | |
| font = EditorGUIUtility.Load("Fonts/RobotoMono/RobotoMono-Regular.ttf") as Font, | |
| fontSize = EditorStyles.label.fontSize + 2, | |
| normal = new GUIStyleState { textColor = Color.white }, | |
| richText = true | |
| }; | |
| } | |
| private static class Contents | |
| { | |
| public static GUIContent live = new GUIContent("Live"); | |
| public static GUIContent showRawMemory = new GUIContent("Display Raw Memory"); | |
| public static GUIContent showBits = new GUIContent("Bits/Hex"); | |
| public static GUIContent showDifferentOnly = new GUIContent("Show Only Differences"); | |
| public static GUIContent[] bufferChoices = | |
| { | |
| new GUIContent("Player (Current)"), | |
| new GUIContent("Player (Previous)"), | |
| new GUIContent("Editor (Current)"), | |
| new GUIContent("Editor (Previous)"), | |
| new GUIContent("Noise Mask"), | |
| new GUIContent("Reset Mask") | |
| }; | |
| } | |
| } | |
| } | |