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