A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR 2using System; 3using System.Collections.Generic; 4using Unity.Collections.LowLevel.Unsafe; 5using UnityEditor; 6using UnityEditor.IMGUI.Controls; 7using UnityEngine.InputSystem.LowLevel; 8 9////TODO: add ability to single-step through events 10 11////TODO: annotate raw memory view with control offset and ranges (probably easiest to put the control tree and raw memory view side by side) 12 13////TODO: find way to automatically dock the state windows next to their InputDeviceDebuggerWindows 14//// (probably needs an extension to the editor UI APIs as the only programmatic docking controls 15//// seem to be through GetWindow) 16 17////TODO: allow setting a C# struct type that we can use to display the layout of the data 18 19////TODO: for delta state events, highlight the controls included in the event (or show only those) 20 21////FIXME: need to prevent extra controls appended at end from reading beyond the state buffer 22 23namespace UnityEngine.InputSystem.Editor 24{ 25 // Additional window that we can pop open to inspect raw state (either on events or on controls/devices). 26 internal class InputStateWindow : EditorWindow 27 { 28 private const int kBytesPerHexGroup = 1; 29 private const int kHexGroupsPerLine = 8; 30 private const int kHexDumpLineHeight = 25; 31 private const int kOffsetLabelWidth = 30; 32 private const int kHexGroupWidth = 25; 33 private const int kBitGroupWidth = 75; 34 35 void Update() 36 { 37 if (m_PollControlState && m_Control != null) 38 { 39 PollBuffersFromControl(m_Control); 40 Repaint(); 41 } 42 } 43 44 public void InitializeWithEvent(InputEventPtr eventPtr, InputControl control) 45 { 46 m_Control = control; 47 m_PollControlState = false; 48 m_StateBuffers = new byte[1][]; 49 m_StateBuffers[0] = GetEventStateBuffer(eventPtr, control); 50 m_SelectedStateBuffer = 0; 51 52 titleContent = new GUIContent(control.displayName); 53 } 54 55 public void InitializeWithEvents(InputEventPtr[] eventPtrs, InputControl control) 56 { 57 var numEvents = eventPtrs.Length; 58 59 m_Control = control; 60 m_PollControlState = false; 61 m_StateBuffers = new byte[numEvents][]; 62 for (var i = 0; i < numEvents; ++i) 63 m_StateBuffers[i] = GetEventStateBuffer(eventPtrs[i], control); 64 m_CompareStateBuffers = true; 65 m_ShowDifferentOnly = true; 66 67 titleContent = new GUIContent(control.displayName); 68 } 69 70 private unsafe byte[] GetEventStateBuffer(InputEventPtr eventPtr, InputControl control) 71 { 72 // Must be an event carrying state. 73 if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>()) 74 throw new ArgumentException("Event must be state or delta event", nameof(eventPtr)); 75 76 // Get state data. 77 void* dataPtr; 78 uint dataSize; 79 uint stateSize; 80 uint stateOffset = 0; 81 82 if (eventPtr.IsA<DeltaStateEvent>()) 83 { 84 var deltaEventPtr = DeltaStateEvent.From(eventPtr); 85 stateSize = control.stateBlock.alignedSizeInBytes; 86 stateOffset = deltaEventPtr->stateOffset; 87 dataPtr = deltaEventPtr->deltaState; 88 dataSize = deltaEventPtr->deltaStateSizeInBytes; 89 } 90 else 91 { 92 var stateEventPtr = StateEvent.From(eventPtr); 93 dataSize = stateSize = stateEventPtr->stateSizeInBytes; 94 dataPtr = stateEventPtr->state; 95 } 96 97 // Copy event data. 98 var buffer = new byte[stateSize]; 99 fixed(byte* bufferPtr = buffer) 100 { 101 UnsafeUtility.MemCpy(bufferPtr + stateOffset, dataPtr, dataSize); 102 } 103 104 return buffer; 105 } 106 107 public unsafe void InitializeWithControl(InputControl control) 108 { 109 m_Control = control; 110 m_PollControlState = true; 111 m_SelectedStateBuffer = (int)BufferSelector.Default; 112 113 PollBuffersFromControl(control, selectBuffer: true); 114 115 titleContent = new GUIContent(control.displayName); 116 } 117 118 private unsafe void PollBuffersFromControl(InputControl control, bool selectBuffer = false) 119 { 120 var bufferChoices = new List<GUIContent>(); 121 var bufferChoiceValues = new List<int>(); 122 123 // Copy front and back buffer state for each update that has valid buffers. 124 var device = control.device; 125 var stateSize = control.m_StateBlock.alignedSizeInBytes; 126 var stateOffset = control.m_StateBlock.byteOffset; 127 m_StateBuffers = new byte[(int)BufferSelector.COUNT][]; 128 for (var i = 0; i < (int)BufferSelector.COUNT; ++i) 129 { 130 var selector = (BufferSelector)i; 131 var deviceState = TryGetDeviceState(device, selector); 132 if (deviceState == null) 133 continue; 134 135 var buffer = new byte[stateSize]; 136 fixed(byte* stateDataPtr = buffer) 137 { 138 UnsafeUtility.MemCpy(stateDataPtr, (byte*)deviceState + (int)stateOffset, stateSize); 139 } 140 m_StateBuffers[i] = buffer; 141 142 if (selectBuffer && m_StateBuffers[m_SelectedStateBuffer] == null) 143 m_SelectedStateBuffer = (int)selector; 144 145 bufferChoices.Add(Contents.bufferChoices[i]); 146 bufferChoiceValues.Add(i); 147 } 148 149 m_BufferChoices = bufferChoices.ToArray(); 150 m_BufferChoiceValues = bufferChoiceValues.ToArray(); 151 } 152 153 private static unsafe void* TryGetDeviceState(InputDevice device, BufferSelector selector) 154 { 155 var manager = InputSystem.s_Manager; 156 var deviceIndex = device.m_DeviceIndex; 157 158 switch (selector) 159 { 160 case BufferSelector.PlayerUpdateFrontBuffer: 161 if (manager.m_StateBuffers.m_PlayerStateBuffers.valid) 162 return manager.m_StateBuffers.m_PlayerStateBuffers.GetFrontBuffer(deviceIndex); 163 break; 164 case BufferSelector.PlayerUpdateBackBuffer: 165 if (manager.m_StateBuffers.m_PlayerStateBuffers.valid) 166 return manager.m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex); 167 break; 168 case BufferSelector.EditorUpdateFrontBuffer: 169 if (manager.m_StateBuffers.m_EditorStateBuffers.valid) 170 return manager.m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex); 171 break; 172 case BufferSelector.EditorUpdateBackBuffer: 173 if (manager.m_StateBuffers.m_EditorStateBuffers.valid) 174 return manager.m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex); 175 break; 176 case BufferSelector.NoiseMaskBuffer: 177 return manager.m_StateBuffers.noiseMaskBuffer; 178 case BufferSelector.ResetMaskBuffer: 179 return manager.m_StateBuffers.resetMaskBuffer; 180 } 181 182 return null; 183 } 184 185 public void OnGUI() 186 { 187 if (m_Control == null) 188 m_ShowRawBytes = true; 189 190 // If our state is no longer valid, just close the window. 191 if (m_StateBuffers == null) 192 { 193 Close(); 194 return; 195 } 196 197 GUILayout.BeginHorizontal(EditorStyles.toolbar); 198 m_PollControlState = GUILayout.Toggle(m_PollControlState, Contents.live, EditorStyles.toolbarButton); 199 200 m_ShowRawBytes = GUILayout.Toggle(m_ShowRawBytes, Contents.showRawMemory, EditorStyles.toolbarButton, 201 GUILayout.Width(150)); 202 203 m_ShowAsBits = GUILayout.Toggle(m_ShowAsBits, Contents.showBits, EditorStyles.toolbarButton); 204 205 if (m_CompareStateBuffers) 206 { 207 var showDifferentOnly = GUILayout.Toggle(m_ShowDifferentOnly, Contents.showDifferentOnly, 208 EditorStyles.toolbarButton, GUILayout.Width(150)); 209 if (showDifferentOnly != m_ShowDifferentOnly && m_ControlTree != null) 210 { 211 m_ControlTree.showDifferentOnly = showDifferentOnly; 212 m_ControlTree.Reload(); 213 } 214 215 m_ShowDifferentOnly = showDifferentOnly; 216 } 217 218 // If we have multiple state buffers to choose from and we're not comparing them to each other, 219 // add dropdown that allows selecting which buffer to display. 220 if (m_StateBuffers.Length > 1 && !m_CompareStateBuffers) 221 { 222 var selectedBuffer = EditorGUILayout.IntPopup(m_SelectedStateBuffer, m_BufferChoices, 223 m_BufferChoiceValues, EditorStyles.toolbarPopup); 224 if (selectedBuffer != m_SelectedStateBuffer) 225 { 226 m_SelectedStateBuffer = selectedBuffer; 227 m_ControlTree = null; 228 } 229 } 230 231 GUILayout.FlexibleSpace(); 232 GUILayout.EndHorizontal(); 233 234 if (m_ShowRawBytes) 235 { 236 DrawHexDump(); 237 } 238 else 239 { 240 if (m_ControlTree == null) 241 { 242 if (m_CompareStateBuffers) 243 { 244 m_ControlTree = InputControlTreeView.Create(m_Control, m_StateBuffers.Length, ref m_ControlTreeState, ref m_ControlTreeHeaderState); 245 m_ControlTree.multipleStateBuffers = m_StateBuffers; 246 m_ControlTree.showDifferentOnly = m_ShowDifferentOnly; 247 } 248 else 249 { 250 m_ControlTree = InputControlTreeView.Create(m_Control, 1, ref m_ControlTreeState, ref m_ControlTreeHeaderState); 251 m_ControlTree.stateBuffer = m_StateBuffers[m_SelectedStateBuffer]; 252 } 253 m_ControlTree.Reload(); 254 m_ControlTree.ExpandAll(); 255 } 256 257 var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true)); 258 m_ControlTree.OnGUI(rect); 259 } 260 } 261 262 private byte[] TryGetBackBufferForCurrentlySelected() 263 { 264 if (m_StateBuffers.Length != (int)BufferSelector.COUNT) 265 return null; 266 267 switch ((BufferSelector)m_SelectedStateBuffer) 268 { 269 case BufferSelector.PlayerUpdateFrontBuffer: 270 return m_StateBuffers[(int)BufferSelector.PlayerUpdateBackBuffer]; 271 case BufferSelector.EditorUpdateFrontBuffer: 272 return m_StateBuffers[(int)BufferSelector.EditorUpdateBackBuffer]; 273 default: 274 return null; 275 } 276 } 277 278 private string FormatByte(byte value) 279 { 280 if (m_ShowAsBits) 281 return Convert.ToString(value, 2).PadLeft(8, '0'); 282 else 283 return value.ToString("X2"); 284 } 285 286 ////TODO: support dumping multiple state side-by-side when comparing 287 private void DrawHexDump() 288 { 289 m_HexDumpScrollPosition = EditorGUILayout.BeginScrollView(m_HexDumpScrollPosition); 290 291 var stateBuffer = m_StateBuffers[m_SelectedStateBuffer]; 292 var prevStateBuffer = TryGetBackBufferForCurrentlySelected(); 293 if (prevStateBuffer != null && prevStateBuffer.Length != stateBuffer.Length) // we assume they're same length, otherwise ignore prev buffer 294 prevStateBuffer = null; 295 var numBytes = stateBuffer.Length; 296 var numHexGroups = numBytes / kBytesPerHexGroup + (numBytes % kBytesPerHexGroup > 0 ? 1 : 0); 297 var numLines = numHexGroups / kHexGroupsPerLine + (numHexGroups % kHexGroupsPerLine > 0 ? 1 : 0); 298 var currentOffset = 0; 299 var currentLineRect = EditorGUILayout.GetControlRect(GUILayout.ExpandWidth(true)); 300 currentLineRect.height = kHexDumpLineHeight; 301 var currentHexGroup = 0; 302 var currentByte = 0; 303 304 ////REVIEW: what would be totally awesome is if this not just displayed a hex dump but also the correlation to current 305 //// control offset assignments 306 307 for (var line = 0; line < numLines; ++line) 308 { 309 // Draw offset. 310 var offsetLabelRect = currentLineRect; 311 offsetLabelRect.width = kOffsetLabelWidth; 312 GUI.Label(offsetLabelRect, currentOffset.ToString(), Styles.offsetLabel); 313 currentOffset += kBytesPerHexGroup * kHexGroupsPerLine; 314 315 // Draw hex groups. 316 var hexGroupRect = offsetLabelRect; 317 hexGroupRect.x += kOffsetLabelWidth + 10; 318 hexGroupRect.width = m_ShowAsBits ? kBitGroupWidth : kHexGroupWidth; 319 for (var group = 0; 320 group < kHexGroupsPerLine && currentHexGroup < numHexGroups; 321 ++group, ++currentHexGroup) 322 { 323 // Convert bytes to hex. 324 var hex = string.Empty; 325 326 for (var i = 0; i < kBytesPerHexGroup; ++i, ++currentByte) 327 { 328 if (currentByte >= numBytes) 329 { 330 hex += " "; 331 continue; 332 } 333 334 var current = FormatByte(stateBuffer[currentByte]); 335 if (prevStateBuffer == null) 336 { 337 hex += current; 338 continue; 339 } 340 341 var prev = FormatByte(prevStateBuffer[currentByte]); 342 if (prev.Length != current.Length) 343 { 344 hex += current; 345 continue; 346 } 347 348 for (var j = 0; j < current.Length; ++j) 349 { 350 if (current[j] != prev[j]) 351 hex += $"<color=#C84B31FF>{current[j]}</color>"; 352 else 353 hex += current[j]; 354 } 355 } 356 357 ////TODO: draw alternating backgrounds for the hex groups 358 359 GUI.Label(hexGroupRect, hex, style: Styles.hexLabel); 360 hexGroupRect.x += m_ShowAsBits ? kBitGroupWidth : kHexGroupWidth; 361 } 362 363 currentLineRect.y += kHexDumpLineHeight; 364 } 365 366 EditorGUILayout.EndScrollView(); 367 } 368 369 // We copy the state we're inspecting to a buffer we own so that we're safe 370 // against any mutations. 371 // When inspecting controls (as opposed to events), we copy all their various 372 // state buffers and allow switching between them. 373 [SerializeField] private byte[][] m_StateBuffers; 374 [SerializeField] private int m_SelectedStateBuffer; 375 [SerializeField] private bool m_CompareStateBuffers; 376 [SerializeField] private bool m_ShowDifferentOnly; 377 [SerializeField] private bool m_ShowRawBytes; 378 [SerializeField] private bool m_ShowAsBits; 379 [SerializeField] private bool m_PollControlState; 380 [SerializeField] private TreeViewState m_ControlTreeState; 381 [SerializeField] private MultiColumnHeaderState m_ControlTreeHeaderState; 382 [SerializeField] private Vector2 m_HexDumpScrollPosition; 383 384 [NonSerialized] private InputControlTreeView m_ControlTree; 385 [NonSerialized] private GUIContent[] m_BufferChoices; 386 [NonSerialized] private int[] m_BufferChoiceValues; 387 388 ////FIXME: we lose this on domain reload; how should we recover? 389 [NonSerialized] private InputControl m_Control; 390 391 private enum BufferSelector 392 { 393 PlayerUpdateFrontBuffer, 394 PlayerUpdateBackBuffer, 395 EditorUpdateFrontBuffer, 396 EditorUpdateBackBuffer, 397 NoiseMaskBuffer, 398 ResetMaskBuffer, 399 COUNT, 400 Default = PlayerUpdateFrontBuffer 401 } 402 403 private static class Styles 404 { 405 public static GUIStyle offsetLabel = new GUIStyle 406 { 407 alignment = TextAnchor.UpperRight, 408 fontStyle = FontStyle.BoldAndItalic, 409 font = EditorStyles.boldFont, 410 fontSize = EditorStyles.boldFont.fontSize - 2, 411 normal = new GUIStyleState { textColor = Color.black } 412 }; 413 414 public static GUIStyle hexLabel = new GUIStyle 415 { 416 fontStyle = FontStyle.Normal, 417 font = EditorGUIUtility.Load("Fonts/RobotoMono/RobotoMono-Regular.ttf") as Font, 418 fontSize = EditorStyles.label.fontSize + 2, 419 normal = new GUIStyleState { textColor = Color.white }, 420 richText = true 421 }; 422 } 423 424 private static class Contents 425 { 426 public static GUIContent live = new GUIContent("Live"); 427 public static GUIContent showRawMemory = new GUIContent("Display Raw Memory"); 428 public static GUIContent showBits = new GUIContent("Bits/Hex"); 429 public static GUIContent showDifferentOnly = new GUIContent("Show Only Differences"); 430 public static GUIContent[] bufferChoices = 431 { 432 new GUIContent("Player (Current)"), 433 new GUIContent("Player (Previous)"), 434 new GUIContent("Editor (Current)"), 435 new GUIContent("Editor (Previous)"), 436 new GUIContent("Noise Mask"), 437 new GUIContent("Reset Mask") 438 }; 439 } 440 } 441} 442#endif // UNITY_EDITOR